foundry-component-library 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Awards/index.tsx +37 -32
- package/lib/components/Awards/styles.module.scss +26 -1
- package/lib/components/StickyTiles/Tile.tsx +72 -0
- package/lib/components/StickyTiles/index.tsx +67 -0
- package/lib/components/StickyTiles/styles.module.scss +165 -0
- package/lib/index.ts +2 -0
- package/lib/queries/getHomePage.ts +10 -0
- package/lib/queries/getPageBySlug.ts +39 -4
- package/package.json +1 -1
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useRef } from "react";
|
|
3
2
|
import styles from "./styles.module.scss";
|
|
4
3
|
import Container from "../Container";
|
|
5
4
|
import { NextImage } from "../../types";
|
|
6
|
-
import useDrag from "../../hooks/useDrag";
|
|
7
5
|
import WavyText from "../TextAnimations/WavyText";
|
|
8
6
|
import { motion } from "framer-motion";
|
|
9
7
|
|
|
8
|
+
// Infinite marquee animation settings
|
|
9
|
+
const marqueeVariants = {
|
|
10
|
+
animate: {
|
|
11
|
+
x: [0, -"100%"],
|
|
12
|
+
transition: {
|
|
13
|
+
x: {
|
|
14
|
+
repeat: Infinity,
|
|
15
|
+
repeatType: "loop",
|
|
16
|
+
duration: 20,
|
|
17
|
+
ease: "linear",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
10
23
|
const Awards = ({
|
|
11
24
|
heading,
|
|
12
25
|
awards,
|
|
@@ -22,49 +35,41 @@ const Awards = ({
|
|
|
22
35
|
}[];
|
|
23
36
|
Image: NextImage;
|
|
24
37
|
}) => {
|
|
25
|
-
|
|
26
|
-
const { handleMouseDown, handleMouseMove, handleMouseUp, dragStyle } =
|
|
27
|
-
useDrag(sectionRef);
|
|
38
|
+
if (!awards) return null;
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
// Duplicate awards to create seamless looping
|
|
41
|
+
const marqueeItems = [...awards, ...awards];
|
|
30
42
|
|
|
31
43
|
return (
|
|
32
44
|
<motion.div
|
|
33
45
|
className={styles.awards}
|
|
34
46
|
initial={{ opacity: 0, y: -5 }}
|
|
35
47
|
animate={{ opacity: 1, y: 0 }}
|
|
36
|
-
transition={{
|
|
37
|
-
duration: 1,
|
|
38
|
-
}}
|
|
39
|
-
>
|
|
48
|
+
transition={{ duration: 1 }}>
|
|
40
49
|
<Container>
|
|
41
50
|
{heading && <WavyText className={styles.heading} text={heading} />}
|
|
42
51
|
</Container>
|
|
43
52
|
<Container noMobilePadding>
|
|
44
|
-
<div
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
>
|
|
53
|
-
{awards.map((award, i) => {
|
|
54
|
-
const { image, heading, text } = award;
|
|
55
|
-
|
|
56
|
-
if (!image) return;
|
|
53
|
+
<div className={styles.marqueeWrapper}>
|
|
54
|
+
<motion.div
|
|
55
|
+
className={styles.marquee}
|
|
56
|
+
variants={marqueeVariants}
|
|
57
|
+
animate="animate">
|
|
58
|
+
{marqueeItems.map((award, i) => {
|
|
59
|
+
const { image, heading, text } = award;
|
|
60
|
+
if (!image) return null;
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
return (
|
|
63
|
+
<div key={image.sourceUrl + i} className={styles.award}>
|
|
64
|
+
<div className={styles.image}>
|
|
65
|
+
{image && <Image src={image.sourceUrl} alt="" fill />}
|
|
66
|
+
</div>
|
|
67
|
+
{heading && <div className={styles.text}>{heading}</div>}
|
|
68
|
+
{text && <div className={styles.client}>{text}</div>}
|
|
62
69
|
</div>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
-
})}
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</motion.div>
|
|
68
73
|
</div>
|
|
69
74
|
</Container>
|
|
70
75
|
</motion.div>
|
|
@@ -21,14 +21,29 @@
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/* NEW — wrapper that clips the scrolling track */
|
|
25
|
+
.marqueeWrapper {
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
width: 100%;
|
|
28
|
+
position: relative;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* NEW — marquee track */
|
|
32
|
+
.marquee {
|
|
25
33
|
display: flex;
|
|
34
|
+
gap: 48px;
|
|
35
|
+
width: max-content;
|
|
36
|
+
animation: marquee 20s linear infinite;
|
|
26
37
|
|
|
27
38
|
@media #{$QUERY-sm} {
|
|
28
39
|
gap: 24px;
|
|
40
|
+
animation-duration: 16s;
|
|
29
41
|
}
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
/* Remove if using marquee (track should scroll) */
|
|
45
|
+
/* .items { display: flex; } */
|
|
46
|
+
|
|
32
47
|
.award {
|
|
33
48
|
width: 20%;
|
|
34
49
|
text-align: center;
|
|
@@ -85,3 +100,13 @@
|
|
|
85
100
|
font-size: 16px;
|
|
86
101
|
}
|
|
87
102
|
}
|
|
103
|
+
|
|
104
|
+
/* NEW — marquee animation: scroll half the track (because items are duplicated) */
|
|
105
|
+
@keyframes marquee {
|
|
106
|
+
0% {
|
|
107
|
+
transform: translateX(0);
|
|
108
|
+
}
|
|
109
|
+
100% {
|
|
110
|
+
transform: translateX(-50%);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { MouseEventHandler, useRef } from "react";
|
|
2
|
+
import styles from "./styles.module.scss";
|
|
3
|
+
import Plus from "../../assets/svg/plus.svg";
|
|
4
|
+
import { NextLink } from "../../types";
|
|
5
|
+
import Arrow from "../../assets/svg/arrow.svg";
|
|
6
|
+
import { useScroll } from "motion/react";
|
|
7
|
+
import { motion, useTransform } from "framer-motion";
|
|
8
|
+
|
|
9
|
+
const Tile = ({
|
|
10
|
+
text,
|
|
11
|
+
background,
|
|
12
|
+
href,
|
|
13
|
+
onClick,
|
|
14
|
+
Link,
|
|
15
|
+
hoverText,
|
|
16
|
+
}: {
|
|
17
|
+
text: string;
|
|
18
|
+
background: "pink" | "yellow" | "brown" | "blue";
|
|
19
|
+
href: string;
|
|
20
|
+
i: number;
|
|
21
|
+
onClick?: MouseEventHandler<HTMLAnchorElement>;
|
|
22
|
+
Link: NextLink;
|
|
23
|
+
hoverText: string;
|
|
24
|
+
isLast: boolean;
|
|
25
|
+
}) => {
|
|
26
|
+
const ref = useRef(null);
|
|
27
|
+
const { scrollYProgress } = useScroll({
|
|
28
|
+
target: ref,
|
|
29
|
+
offset: ["start 0.8", "start 0.3"],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const scale = useTransform(scrollYProgress, [0, 1], [0.8, 1]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<motion.div
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={styles.tileWrapper}
|
|
38
|
+
style={{
|
|
39
|
+
scale: scale,
|
|
40
|
+
}}>
|
|
41
|
+
<div className={styles.videoWrapper}>
|
|
42
|
+
<video
|
|
43
|
+
className={styles.video}
|
|
44
|
+
autoPlay
|
|
45
|
+
loop
|
|
46
|
+
playsInline
|
|
47
|
+
src="https://data.foundry.ch/wp-content/uploads/2025/07/foundry-sennheiser-hear-more-thumbnail-video.mp4"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<Link
|
|
51
|
+
href={href}
|
|
52
|
+
onClick={onClick}
|
|
53
|
+
draggable="false"
|
|
54
|
+
className={`${styles.tile} ${styles[background]}`}>
|
|
55
|
+
<div className={styles.face}>
|
|
56
|
+
<div className={styles.text}>{text}</div>
|
|
57
|
+
<div>
|
|
58
|
+
<Plus />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div className={styles.tails}>
|
|
62
|
+
<div className={styles.tailsText}>{hoverText}</div>
|
|
63
|
+
<div>
|
|
64
|
+
Learn More <Arrow />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</Link>
|
|
68
|
+
</motion.div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default Tile;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import Container from "../Container";
|
|
4
|
+
import styles from "./styles.module.scss";
|
|
5
|
+
import TextSection from "../TextSection";
|
|
6
|
+
import Tile from "./Tile";
|
|
7
|
+
import { NextLink } from "../../types";
|
|
8
|
+
import { motion } from "framer-motion";
|
|
9
|
+
|
|
10
|
+
const ServiceHubsTeaser = ({
|
|
11
|
+
caption,
|
|
12
|
+
heading,
|
|
13
|
+
text,
|
|
14
|
+
tiles,
|
|
15
|
+
Link,
|
|
16
|
+
}: {
|
|
17
|
+
caption: string;
|
|
18
|
+
heading: string;
|
|
19
|
+
text: string;
|
|
20
|
+
tiles: {
|
|
21
|
+
id: string;
|
|
22
|
+
uri: string;
|
|
23
|
+
title: string;
|
|
24
|
+
customFieldsHub: {
|
|
25
|
+
heading: string;
|
|
26
|
+
};
|
|
27
|
+
}[];
|
|
28
|
+
Link: NextLink;
|
|
29
|
+
}) => {
|
|
30
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={styles.benefits}>
|
|
34
|
+
<TextSection caption={caption} heading={heading} text={text} isSmall />
|
|
35
|
+
<Container noMobilePadding>
|
|
36
|
+
<div className={styles.wrapper} ref={wrapperRef}>
|
|
37
|
+
<motion.div className={styles.tiles}>
|
|
38
|
+
{tiles.map((tile, i) => {
|
|
39
|
+
const colors: Array<"pink" | "yellow" | "brown" | "blue"> = [
|
|
40
|
+
"pink",
|
|
41
|
+
"yellow",
|
|
42
|
+
"brown",
|
|
43
|
+
"blue",
|
|
44
|
+
];
|
|
45
|
+
const background = colors[i % colors.length];
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Tile
|
|
49
|
+
key={tile.id}
|
|
50
|
+
text={tile.title}
|
|
51
|
+
hoverText={tile.customFieldsHub.heading}
|
|
52
|
+
background={background}
|
|
53
|
+
href={tile.uri}
|
|
54
|
+
i={i}
|
|
55
|
+
Link={Link}
|
|
56
|
+
isLast={i === tiles.length - 1}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</motion.div>
|
|
61
|
+
</div>
|
|
62
|
+
</Container>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default ServiceHubsTeaser;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
@use "../variables" as *;
|
|
2
|
+
|
|
3
|
+
.benefits {
|
|
4
|
+
// overflow: hidden;
|
|
5
|
+
margin-bottom: 80px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.wrapper {
|
|
9
|
+
width: 100%;
|
|
10
|
+
// overflow-x: scroll;
|
|
11
|
+
user-select: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.tiles {
|
|
15
|
+
gap: 16px;
|
|
16
|
+
padding-bottom: 64px;
|
|
17
|
+
|
|
18
|
+
@media #{$QUERY-sm} {
|
|
19
|
+
padding-bottom: 32px;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tileWrapper {
|
|
24
|
+
z-index: 0;
|
|
25
|
+
position: relative;
|
|
26
|
+
margin-left: auto;
|
|
27
|
+
margin-right: auto;
|
|
28
|
+
position: sticky;
|
|
29
|
+
top: 90px;
|
|
30
|
+
height: 80vh;
|
|
31
|
+
width: 800px;
|
|
32
|
+
max-width: 90%;
|
|
33
|
+
|
|
34
|
+
@media screen and (max-width: $screen-sm) {
|
|
35
|
+
height: 500px;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.videoWrapper {
|
|
40
|
+
position: absolute;
|
|
41
|
+
top: 0;
|
|
42
|
+
left: 0;
|
|
43
|
+
width: 100%;
|
|
44
|
+
height: 100%;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.video {
|
|
48
|
+
width: 100%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
object-fit: cover;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.tile {
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
white-space: pre-wrap;
|
|
56
|
+
padding: 16px;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
text-align: center;
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
background-color: rgba($color-pink, 1);
|
|
64
|
+
color: $color-brown;
|
|
65
|
+
font-size: 34px;
|
|
66
|
+
width: 100%;
|
|
67
|
+
height: 100%;
|
|
68
|
+
font-family: $font-secondary;
|
|
69
|
+
box-shadow: 0 0 10px rgba($color-brown, 0.2);
|
|
70
|
+
position: relative;
|
|
71
|
+
z-index: 2;
|
|
72
|
+
transition: background-color 0.3s;
|
|
73
|
+
|
|
74
|
+
path {
|
|
75
|
+
stroke: $color-brown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&.yellow {
|
|
79
|
+
background-color: rgba($color-yellow, 1);
|
|
80
|
+
color: $color-blue;
|
|
81
|
+
box-shadow: 0 0 10px rgba($color-blue, 0.2);
|
|
82
|
+
|
|
83
|
+
path {
|
|
84
|
+
stroke: $color-blue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&.brown {
|
|
89
|
+
background-color: rgba($color-brown, 1);
|
|
90
|
+
color: $color-pink;
|
|
91
|
+
box-shadow: 0 0 10px rgba($color-pink, 0.2);
|
|
92
|
+
|
|
93
|
+
path {
|
|
94
|
+
stroke: $color-pink;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
&.blue {
|
|
99
|
+
background-color: rgba($color-blue, 1);
|
|
100
|
+
color: $color-white;
|
|
101
|
+
box-shadow: 0 0 10px rgba($color-black, 0.2);
|
|
102
|
+
|
|
103
|
+
path {
|
|
104
|
+
stroke: $color-white;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&:hover {
|
|
109
|
+
background-color: rgba($color-pink, 0.2);
|
|
110
|
+
|
|
111
|
+
&.yellow {
|
|
112
|
+
background-color: rgba($color-yellow, 0.2);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
&.brown {
|
|
116
|
+
background-color: rgba($color-brown, 0.2);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
&.blue {
|
|
120
|
+
background-color: rgba($color-blue, 0.2);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.face {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.tails {
|
|
128
|
+
display: block;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.text {
|
|
134
|
+
margin-bottom: 42px;
|
|
135
|
+
|
|
136
|
+
&:last-child {
|
|
137
|
+
margin-bottom: 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.face {
|
|
142
|
+
display: block;
|
|
143
|
+
font-size: 42px;
|
|
144
|
+
|
|
145
|
+
@media #{$QUERY-sm} {
|
|
146
|
+
font-size: 34px;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.tails {
|
|
151
|
+
padding-left: 30px;
|
|
152
|
+
display: none;
|
|
153
|
+
font-size: 34px;
|
|
154
|
+
font-family: $font-secondary;
|
|
155
|
+
color: $color-white;
|
|
156
|
+
|
|
157
|
+
path {
|
|
158
|
+
stroke: $color-white !important;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.tailsText {
|
|
163
|
+
margin-bottom: 30px;
|
|
164
|
+
max-width: 500px;
|
|
165
|
+
}
|
package/lib/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import TeamBenefits from "./components/TeamBenefits";
|
|
|
37
37
|
import TeamPhotos from "./components/TeamPhotos";
|
|
38
38
|
import TextSection from "./components/TextSection";
|
|
39
39
|
import Tiles from "./components/Tiles";
|
|
40
|
+
import StickyTiles from "./components/StickyTiles";
|
|
40
41
|
import VideoTeaser from "./components/VideoTeaser";
|
|
41
42
|
|
|
42
43
|
export {
|
|
@@ -79,5 +80,6 @@ export {
|
|
|
79
80
|
TeamPhotos,
|
|
80
81
|
TextSection,
|
|
81
82
|
Tiles,
|
|
83
|
+
StickyTiles,
|
|
82
84
|
VideoTeaser,
|
|
83
85
|
};
|
|
@@ -113,6 +113,11 @@ type HomePage = {
|
|
|
113
113
|
sourceUrl: string;
|
|
114
114
|
};
|
|
115
115
|
}[];
|
|
116
|
+
quotes?: {
|
|
117
|
+
name?: string;
|
|
118
|
+
position?: string;
|
|
119
|
+
text?: string;
|
|
120
|
+
}[];
|
|
116
121
|
};
|
|
117
122
|
};
|
|
118
123
|
};
|
|
@@ -260,6 +265,11 @@ export default async function getHomePage({
|
|
|
260
265
|
sourceUrl
|
|
261
266
|
}
|
|
262
267
|
}
|
|
268
|
+
quotes {
|
|
269
|
+
name
|
|
270
|
+
position
|
|
271
|
+
text
|
|
272
|
+
}
|
|
263
273
|
}
|
|
264
274
|
}
|
|
265
275
|
}
|
|
@@ -1,19 +1,54 @@
|
|
|
1
1
|
import { gql } from "graphql-request";
|
|
2
2
|
import { Page } from "../../lib/types";
|
|
3
3
|
import client from "./client";
|
|
4
|
+
import { ContactPage } from "./getContactPage";
|
|
4
5
|
|
|
5
6
|
export default async function getPageBySlug(
|
|
6
|
-
slug: string
|
|
7
|
-
|
|
7
|
+
slug: string,
|
|
8
|
+
language: string
|
|
9
|
+
): Promise<{ page: Page; contactPage: ContactPage }> {
|
|
10
|
+
const contactPage = language === "DE" ? "contact-de" : "contact";
|
|
11
|
+
|
|
8
12
|
const query = gql`
|
|
9
13
|
query GetPageBySlug($slug: ID!) {
|
|
10
14
|
page(id: $slug, idType: URI) {
|
|
11
15
|
title
|
|
16
|
+
content
|
|
17
|
+
}
|
|
18
|
+
contactPage: page(id: "${contactPage}", idType: URI) {
|
|
19
|
+
customFieldsContact {
|
|
20
|
+
facebook
|
|
21
|
+
instagram
|
|
22
|
+
linkedin
|
|
23
|
+
berlinImage {
|
|
24
|
+
sourceUrl
|
|
25
|
+
}
|
|
26
|
+
berlinText
|
|
27
|
+
berlinEmail
|
|
28
|
+
berlinPhone
|
|
29
|
+
zurichImage {
|
|
30
|
+
sourceUrl
|
|
31
|
+
}
|
|
32
|
+
zurichText
|
|
33
|
+
zurichEmail
|
|
34
|
+
zurichPhone
|
|
35
|
+
newyorkImage {
|
|
36
|
+
sourceUrl
|
|
37
|
+
}
|
|
38
|
+
newyorkText
|
|
39
|
+
newyorkEmail
|
|
40
|
+
newyorkPhone
|
|
41
|
+
contactTeaserHeading
|
|
42
|
+
contactTeaserText
|
|
43
|
+
}
|
|
12
44
|
}
|
|
13
45
|
}
|
|
14
46
|
`;
|
|
15
47
|
|
|
16
48
|
const variables = { slug };
|
|
17
|
-
const data: { page: Page } = await client.request(
|
|
18
|
-
|
|
49
|
+
const data: { page: Page; contactPage: ContactPage } = await client.request(
|
|
50
|
+
query,
|
|
51
|
+
variables
|
|
52
|
+
);
|
|
53
|
+
return data;
|
|
19
54
|
}
|