Skip to content

Commit 2c2450a

Browse files
committed
integration responsive
1 parent 812eb62 commit 2c2450a

3 files changed

Lines changed: 163 additions & 89 deletions

File tree

.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ module.exports = {
99
'plugin:nuxt/recommended',
1010
'codex',
1111
],
12+
rules: {
13+
'jsdoc/require-returns': 'off',
14+
},
1215
};

assets/styles/variables.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@custom-media --screen-small (max-width: 1024px);
33
@custom-media --screen-mobile (max-width: 850px);
44
@custom-media --screen-mobile-extra-small (max-width: 320px);
5+
@custom-media --screen-desktop (min-width: 851px);
56

67
:root {
78
--color-bg-main: #000000;

components/integrations.vue

Lines changed: 159 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,102 +2,45 @@
22
<div ref="integrations" class="integrations" :class="{ 'integrations--visible': isVisible }">
33
<div class="integrations__row">
44
<div
5-
v-for="n in lineLength"
5+
v-for="n in totalItemsPerLine"
66
:key="n"
77
class="integrations__item"
88
/>
99
</div>
1010

11-
<!-- First line -->
12-
<div class="integrations__row">
13-
<template v-if="Math.floor((lineLength - firstLine.length) / 2) > 0">
14-
<div
15-
v-for="n in Math.floor((lineLength - firstLine.length) / 2)"
16-
:key="`first-line-before-${n}`"
17-
class="integrations__item"
18-
/>
19-
</template>
11+
<!-- Dynamic lines -->
12+
<div
13+
v-for="(line, lineIndex) in lines"
14+
:key="`line-${lineIndex}`"
15+
class="integrations__row"
16+
>
2017
<div
21-
v-for="(integration, index) in firstLine"
22-
:key="integration.name"
18+
v-for="n in Math.max(1, Math.floor((totalItemsPerLine - line.length) / 2))"
19+
:key="`line-${lineIndex}-before-${n}`"
2320
class="integrations__item"
24-
:style="{
25-
'--glow-color': integration.glowColor,
26-
'--animation-delay': `${index * 0.1}s`,
27-
}"
28-
>
29-
<img :src="integration.picture" :alt="integration.name">
30-
</div>
31-
<template v-if="Math.ceil((lineLength - firstLine.length) / 2) + 1 > 0">
32-
<div
33-
v-for="n in Math.ceil((lineLength - firstLine.length) / 2) + 1"
34-
:key="`first-line-after-${n}`"
35-
class="integrations__item"
36-
/>
37-
</template>
38-
</div>
39-
40-
<!-- Second line -->
41-
<div class="integrations__row">
42-
<template v-if="Math.ceil((lineLength - secondLine.length) / 2) > 0">
43-
<div
44-
v-for="n in Math.ceil((lineLength - secondLine.length) / 2)"
45-
:key="`second-line-before-${n}`"
46-
class="integrations__item"
47-
/>
48-
</template>
21+
/>
4922
<div
50-
v-for="(integration, index) in secondLine"
23+
v-for="(integration, index) in line"
5124
:key="integration.name"
5225
class="integrations__item"
5326
:style="{
5427
'--glow-color': integration.glowColor,
55-
'--animation-delay': `${(index + 5) * 0.1}s`,
28+
'--animation-delay': `${(lineIndex * maxItemsPerLine + index) * 0.1}s`,
5629
}"
5730
>
5831
<img :src="integration.picture" :alt="integration.name">
5932
</div>
60-
<template v-if="Math.floor((lineLength - secondLine.length) / 2) + 1 > 0">
61-
<div
62-
v-for="n in Math.ceil((lineLength - secondLine.length) / 2) + 1"
63-
:key="`second-line-after-${n}`"
64-
class="integrations__item"
65-
/>
66-
</template>
67-
</div>
6833

69-
<!-- Third line -->
70-
<div class="integrations__row">
71-
<template v-if="Math.floor((lineLength - thirdLine.length) / 2) > 0">
72-
<div
73-
v-for="n in Math.floor((lineLength - thirdLine.length) / 2)"
74-
:key="`third-line-before-${n}`"
75-
class="integrations__item"
76-
/>
77-
</template>
7834
<div
79-
v-for="(integration, index) in thirdLine"
80-
:key="integration.name"
35+
v-for="n in Math.max(1, Math.ceil((totalItemsPerLine - line.length) / 2) + 1)"
36+
:key="`line-${lineIndex}-after-${n}`"
8137
class="integrations__item"
82-
:style="{
83-
'--glow-color': integration.glowColor,
84-
'--animation-delay': `${(index + 13) * 0.1}s`,
85-
}"
86-
>
87-
<img :src="integration.picture" :alt="integration.name">
88-
</div>
89-
<template v-if="Math.floor((lineLength - thirdLine.length) / 2) + 1 > 0">
90-
<div
91-
v-for="n in Math.ceil((lineLength - thirdLine.length) / 2) + 1"
92-
:key="`third-line-after-${n}`"
93-
class="integrations__item"
94-
/>
95-
</template>
38+
/>
9639
</div>
9740

9841
<div class="integrations__row">
9942
<div
100-
v-for="n in lineLength"
43+
v-for="n in totalItemsPerLine"
10144
:key="n"
10245
class="integrations__item"
10346
/>
@@ -128,8 +71,26 @@ import webpackSvg from '~/assets/svg/integrations/webpack.svg';
12871
import viteSvg from '~/assets/svg/integrations/vite.svg';
12972
import sentrySvg from '~/assets/svg/integrations/sentry.svg';
13073
74+
interface Integration {
75+
name: string;
76+
picture: string;
77+
link: string;
78+
glowColor: string;
79+
}
80+
81+
type Data = {
82+
integrations: Integration[];
83+
windowWidth: number;
84+
isVisible: boolean;
85+
observer: IntersectionObserver | null;
86+
mobileBreakpoint: number;
87+
itemsWidth: number;
88+
itemsGap: number;
89+
containerPadding: number;
90+
};
91+
13192
export default Vue.extend({
132-
data() {
93+
data: function (): Data {
13394
return {
13495
integrations: [
13596
{
@@ -245,27 +206,117 @@ export default Vue.extend({
245206
isVisible: false,
246207
observer: null as IntersectionObserver | null,
247208
mobileBreakpoint: 850,
209+
itemsWidth: 80,
210+
itemsGap: 20,
211+
containerPadding: 2 * 20,
248212
};
249213
},
250214
computed: {
251-
lineLength() {
252-
const itemWidth = 80;
253-
const itemsGap = 20;
254-
const lineWidth = this.windowWidth - 2 * 20;
215+
/**
216+
* Maximum number of items per line
217+
* Used for the first and last empty lines
218+
*/
219+
totalItemsPerLine: function (): number {
220+
return Math.ceil(this.windowWidth / (this.itemsWidth + this.itemsGap));
221+
},
255222
256-
return Math.ceil(lineWidth / (itemWidth + itemsGap));
223+
/**
224+
* How many non-empty items should be in the line
225+
* Based on the screen width
226+
*/
227+
maxItemsPerLine: function (): number {
228+
// Use 30% of screen width on desktop, 100% on mobile
229+
const isMobile = this.windowWidth < this.mobileBreakpoint;
230+
const maxWidth = isMobile ? this.windowWidth - this.containerPadding : this.windowWidth * 0.3;
231+
232+
return Math.ceil(maxWidth / (this.itemsWidth + this.itemsGap));
257233
},
258-
isMobile() {
234+
235+
/**
236+
* Whether the screen is mobile
237+
*/
238+
isMobile: function (): boolean {
259239
return this.windowWidth < this.mobileBreakpoint;
260240
},
261-
firstLine() {
262-
return this.integrations.slice(0, 5);
263-
},
264-
secondLine() {
265-
return this.integrations.slice(5, 13);
241+
242+
/**
243+
* How many lines should be in the component
244+
* Based on the max items per line
245+
*/
246+
totalLines: function (): number {
247+
// Calculate how many lines we need based on total items and items per line
248+
const totalItems = this.integrations.length;
249+
const maxItemsPerLine = this.maxItemsPerLine;
250+
251+
return Math.ceil(totalItems / maxItemsPerLine);
266252
},
267-
thirdLine() {
268-
return this.integrations.slice(13);
253+
254+
/**
255+
* Separation by lines
256+
*/
257+
lines: function (): Integration[][] {
258+
const totalItems = this.integrations.length;
259+
const targetItemsPerLine = this.maxItemsPerLine;
260+
const lines = [];
261+
262+
if (this.isMobile) {
263+
// On mobile, use normal distribution
264+
const totalLines = Math.ceil(totalItems / targetItemsPerLine);
265+
const baseItemsPerLine = Math.floor(totalItems / totalLines);
266+
const remainder = totalItems % totalLines;
267+
268+
for (let i = 0; i < totalLines; i++) {
269+
const startIndex = i * baseItemsPerLine + Math.min(i, remainder);
270+
const endIndex = startIndex + baseItemsPerLine + (i < remainder ? 1 : 0);
271+
272+
lines.push(this.integrations.slice(startIndex, endIndex));
273+
}
274+
} else {
275+
// On desktop, make first and last lines have -1 item, center lines have +2 items
276+
const totalLines = Math.ceil(totalItems / targetItemsPerLine);
277+
278+
if (totalLines <= 2) {
279+
// If only 1-2 lines, use normal distribution
280+
const baseItemsPerLine = Math.floor(totalItems / totalLines);
281+
const remainder = totalItems % totalLines;
282+
283+
for (let i = 0; i < totalLines; i++) {
284+
const startIndex = i * baseItemsPerLine + Math.min(i, remainder);
285+
const endIndex = startIndex + baseItemsPerLine + (i < remainder ? 1 : 0);
286+
287+
lines.push(this.integrations.slice(startIndex, endIndex));
288+
}
289+
} else {
290+
// For 3+ lines, redistribute items
291+
const itemsPerLine = Math.floor(totalItems / totalLines);
292+
const remainder = totalItems % totalLines;
293+
let currentIndex = 0;
294+
295+
for (let i = 0; i < totalLines; i++) {
296+
let itemsInThisLine;
297+
298+
if (i === 0 || i === totalLines - 1) {
299+
// First and last lines get -1 item
300+
itemsInThisLine = Math.max(1, itemsPerLine - 1);
301+
} else {
302+
// Center lines get +2 items (if available)
303+
itemsInThisLine = itemsPerLine + 2;
304+
}
305+
306+
// Adjust for remainder
307+
if (i < remainder) {
308+
itemsInThisLine += 1;
309+
}
310+
311+
const endIndex = Math.min(currentIndex + itemsInThisLine, totalItems);
312+
313+
lines.push(this.integrations.slice(currentIndex, endIndex));
314+
currentIndex = endIndex;
315+
}
316+
}
317+
}
318+
319+
return lines;
269320
},
270321
},
271322
mounted() {
@@ -286,11 +337,11 @@ export default Vue.extend({
286337
},
287338
288339
methods: {
289-
handleResize() {
340+
handleResize: function (): void {
290341
this.windowWidth = window.innerWidth;
291342
},
292343
293-
setupIntersectionObserver() {
344+
setupIntersectionObserver: function (): void {
294345
this.observer = new IntersectionObserver(
295346
(entries) => {
296347
entries.forEach((entry) => {
@@ -313,6 +364,18 @@ export default Vue.extend({
313364
this.observer.observe(element);
314365
}
315366
},
367+
368+
calculateItemsPerLine: function (): number {
369+
const itemWidth = 80;
370+
const itemsGap = 20;
371+
const containerPadding = 2 * 20;
372+
373+
// Use 60% of screen width on desktop, 100% on mobile
374+
const isMobile = this.windowWidth < this.mobileBreakpoint;
375+
const maxWidth = isMobile ? this.windowWidth - containerPadding : this.windowWidth * 0.6;
376+
377+
return Math.ceil(maxWidth / (itemWidth + itemsGap));
378+
},
316379
},
317380
});
318381
</script>
@@ -329,6 +392,7 @@ export default Vue.extend({
329392
width: 100%;
330393
overflow: hidden;
331394
position: relative;
395+
color:red;
332396
333397
mask-image: linear-gradient(90deg,transparent 0%,#ffffff 300px,#ffffff calc(100vw - 300px),transparent 100%);
334398
@@ -364,6 +428,12 @@ export default Vue.extend({
364428
&:nth-of-type(2n+1) {
365429
margin-left: -50px;
366430
}
431+
432+
@media (--screen-mobile-extra-small) {
433+
&:nth-of-type(2n) {
434+
margin-left: -100px;
435+
}
436+
}
367437
}
368438
369439
&__item {

0 commit comments

Comments
 (0)