Skip to content

Commit 9c17c61

Browse files
feat: build /careers/early-careers page (#2743)
* feat: build /careers/easrly-careers page * fix: linter errors * refactor: apply suggestions from copilot * fix: add spaing between achievement cards on small and medium screens * address UX comments * address Design comments * address code review comments --------- Co-authored-by: Muhammad Ali <muhammad.ali@canonical.com> Co-authored-by: Muhammad Ali <its.muhammad.ali.mughal@gmail.com>
1 parent c296041 commit 9c17c61

11 files changed

Lines changed: 976 additions & 1 deletion

File tree

build.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ let entries = {
2121
"events": "./static/js/events.js",
2222
"in-page-navigation": "./static/js/in-page-navigation.js",
2323
"prism": "./static/js/prism.js",
24+
"early-careers-animation": "./static/js/early-careers-animation.js",
2425
};
2526

2627
const isDev = process && process.env && process.env.NODE_ENV === "development";

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@canonical/react-components": "^0.60.0",
3131
"@fullhuman/postcss-purgecss": "6.0.0",
3232
"@lottiefiles/dotlottie-web": "^0.44.0",
33+
"@rive-app/canvas": "^2.38.5",
3334
"@tanstack/react-query": "^5.83.0",
3435
"@testing-library/cypress": "9.0.0",
3536
"autoprefixer": "10.4.21",
@@ -86,6 +87,10 @@
8687
"transform": {
8788
"^.+\\.(ts|tsx|js|jsx)$": "ts-jest"
8889
},
89-
"coverageReporters": ["cobertura", "text", "html"]
90+
"coverageReporters": [
91+
"cobertura",
92+
"text",
93+
"html"
94+
]
9095
}
9196
}

scripts/build-modules.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@ mkdir -p static/js/modules/vanilla-framework/js
3232
cp -r node_modules/vanilla-framework/templates/_macros/ static/js/modules/vanilla-framework
3333
cp -r node_modules/vanilla-framework/templates/static/js/tabs.js static/js/modules/vanilla-framework/js/.
3434

35+
mkdir -p static/js/modules/rive
36+
cp node_modules/@rive-app/canvas/rive.wasm static/js/modules/rive/
37+
3538
mkdir -p static/js/dist/prism-components && cp -r node_modules/prismjs/components/*.min.js static/js/dist/prism-components/
1.05 MB
Binary file not shown.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Rive, Fit, Alignment, Layout, RuntimeLoader } from "@rive-app/canvas";
2+
3+
// Self-host the WASM backing dependency so we don't rely on a third-party CDN
4+
// at runtime and stay within the site's CSP (connect-src 'self')
5+
RuntimeLoader.setWasmUrl("/static/js/modules/rive/rive.wasm");
6+
7+
function initCareerProgressionAnimation() {
8+
const canvas = document.getElementById("rive-career-progression");
9+
10+
if (!canvas) {
11+
return;
12+
}
13+
14+
const rivSrc = canvas.dataset.rivSrc;
15+
16+
if (!rivSrc) {
17+
return;
18+
}
19+
20+
const prefersReducedMotion = window.matchMedia(
21+
"(prefers-reduced-motion: reduce)"
22+
).matches;
23+
24+
// Track async state: the Rive WASM load and the scroll intersection can
25+
// complete in either order, so we reconcile them with flags.
26+
let hasLoaded = false;
27+
let isInView = false;
28+
29+
const riveInstance = new Rive({
30+
src: rivSrc,
31+
canvas: canvas,
32+
33+
autoplay: !prefersReducedMotion,
34+
stateMachines: "State Machine 1",
35+
layout: new Layout({ fit: Fit.Cover, alignment: Alignment.Center }),
36+
onLoad: () => {
37+
riveInstance.resizeDrawingSurfaceToCanvas();
38+
hasLoaded = true;
39+
// If the canvas is already off-screen when onLoad fires, pause now.
40+
if (!isInView && !prefersReducedMotion) {
41+
riveInstance.pause();
42+
}
43+
},
44+
});
45+
46+
// Only play the animation when the canvas is in view
47+
if (!prefersReducedMotion) {
48+
const observer = new IntersectionObserver(
49+
(entries) => {
50+
entries.forEach((entry) => {
51+
isInView = entry.isIntersecting;
52+
if (!hasLoaded) {
53+
return;
54+
}
55+
if (isInView) {
56+
riveInstance.play("State Machine 1");
57+
} else {
58+
riveInstance.pause();
59+
}
60+
});
61+
},
62+
{ threshold: 0.1 }
63+
);
64+
65+
observer.observe(canvas);
66+
}
67+
68+
window.addEventListener("resize", () => {
69+
riveInstance.resizeDrawingSurfaceToCanvas();
70+
});
71+
}
72+
73+
if (document.readyState !== "loading") {
74+
initCareerProgressionAnimation();
75+
} else {
76+
document.addEventListener("DOMContentLoaded", initCareerProgressionAnimation);
77+
}

static/sass/_pattern_cards.scss

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,49 @@
11
@mixin canonical-p-card {
22
@include selectable-card;
3+
@include achievement-card;
4+
}
5+
6+
@mixin achievement-card {
7+
// Card used to highlight an individual's achievement on /careers/early-careers.
8+
.p-card--achievement {
9+
@extend %vf-card;
10+
11+
border: 1px solid $color-mid-light;
12+
display: flex;
13+
flex-direction: column;
14+
height: 100%;
15+
margin-bottom: 0;
16+
padding-left: 0;
17+
padding-right: 0;
18+
}
19+
20+
// Hack the grid to add vertical spacing between rows of achievement cards on screens where
21+
.p-achievement-cards-grid {
22+
@media screen and (width < $breakpoint-large) {
23+
row-gap: $spv--large;
24+
}
25+
}
26+
27+
.p-card--achievement__icon {
28+
align-self: flex-start;
29+
display: block;
30+
height: 2.5rem;
31+
margin-bottom: 1rem;
32+
margin-left: $spv--large;
33+
width: auto;
34+
}
35+
36+
.p-card--achievement__content {
37+
flex: 1 1 auto;
38+
padding-left: $spv--large;
39+
padding-right: $spv--large;
40+
}
41+
42+
.p-card--achievement__footer {
43+
border-top: 1px solid $color-mid-light;
44+
margin-top: 1rem;
45+
padding: 1rem $spv--large 0;
46+
}
347
}
448

549
@mixin selectable-card {

static/sass/_pattern_carousel.scss

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,134 @@
8686
}
8787
}
8888
}
89+
90+
@mixin collaboration-carousel {
91+
// Custom carousel used on /careers/early-careers
92+
.p-collaboration-carousel__inner {
93+
overflow: hidden;
94+
padding-top: 3rem;
95+
position: relative;
96+
}
97+
98+
.p-collaboration-carousel__controls {
99+
position: absolute;
100+
right: 0;
101+
top: 6px;
102+
white-space: nowrap;
103+
z-index: 2;
104+
display: flex;
105+
gap: 8px;
106+
}
107+
108+
.p-collaboration-carousel__previous,
109+
.p-collaboration-carousel__next {
110+
&.is-disabled {
111+
opacity: 0.3;
112+
cursor: not-allowed;
113+
}
114+
}
115+
116+
.p-collaboration-carousel__track {
117+
display: flex;
118+
gap: 1rem;
119+
transition: transform 0.45s ease;
120+
will-change: transform;
121+
}
122+
123+
.p-collaboration-carousel__image {
124+
filter: grayscale(100%);
125+
height: 100%;
126+
object-fit: cover;
127+
transition: filter 0.45s ease;
128+
width: 100%;
129+
}
130+
131+
.p-collaboration-carousel__cell {
132+
flex: 0 0 26%;
133+
position: relative;
134+
transition: flex-basis 0.45s ease;
135+
136+
&.is-selected {
137+
flex-basis: 56%;
138+
139+
.p-collaboration-carousel__image {
140+
filter: none;
141+
}
142+
}
143+
}
144+
145+
.p-collaboration-carousel__media {
146+
height: 24rem;
147+
overflow: hidden;
148+
149+
@media (max-width: $breakpoint-small) {
150+
height: 15rem;
151+
}
152+
}
153+
154+
.p-collaboration-carousel__titles {
155+
left: 0;
156+
position: absolute;
157+
top: 0;
158+
width: 100%;
159+
z-index: 1;
160+
}
161+
162+
.p-collaboration-carousel__title {
163+
opacity: 0;
164+
padding-right: 4rem;
165+
position: absolute;
166+
top: 0;
167+
transition: opacity 0.45s ease;
168+
169+
&.is-selected {
170+
opacity: 1;
171+
}
172+
}
173+
174+
.p-collaboration-carousel__captions {
175+
margin-top: 1.5rem;
176+
min-height: 9rem;
177+
overflow: hidden;
178+
position: relative;
179+
180+
@media (max-width: $breakpoint-small) {
181+
min-height: 15rem;
182+
}
183+
}
184+
185+
.p-collaboration-carousel__description {
186+
left: 0;
187+
position: absolute;
188+
top: 0;
189+
transition: transform 0.45s ease;
190+
will-change: transform;
191+
}
192+
}
193+
194+
@mixin quote-slider {
195+
@keyframes quote-slide-in {
196+
from {
197+
opacity: 0;
198+
transform: translateY(1rem);
199+
}
200+
201+
to {
202+
opacity: 1;
203+
transform: translateY(0);
204+
}
205+
}
206+
207+
@keyframes quote-fade-out {
208+
from { opacity: 1; }
209+
to { opacity: 0; }
210+
}
211+
212+
.js-quote-slide--animating-in {
213+
animation: quote-slide-in 0.3s ease forwards;
214+
}
215+
216+
.js-quote-slide--animating-out {
217+
animation: quote-fade-out 0.1s ease forwards;
218+
}
219+
}

static/sass/styles.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ $p-small-lh-diff: map-get($line-heights, default-text) -
6868
@include canonical-p-navigation-dropdown-content;
6969
@include canonical-p-navigation-mobile;
7070
@include sprint-carousel;
71+
@include collaboration-carousel;
72+
@include quote-slider;
7173
@include homepage-styles;
7274
@include pattern-venobox;
7375
@include maas-styles;
@@ -550,6 +552,22 @@ $p-small-lh-diff: map-get($line-heights, default-text) -
550552
width: 100%;
551553
}
552554

555+
.p-rive-animation {
556+
position: relative;
557+
width: 100%;
558+
559+
&__canvas {
560+
aspect-ratio: 1200 / 480;
561+
display: block;
562+
width: 100%;
563+
}
564+
}
565+
566+
.p-quote-slider__controls {
567+
display: flex;
568+
gap: 8px;
569+
}
570+
553571
.p-testimonial {
554572
@extend %fixed-width-container;
555573

0 commit comments

Comments
 (0)