Skip to content
This repository was archived by the owner on Jan 28, 2026. It is now read-only.

Commit 276e79f

Browse files
committed
Merge branch 'feat/sliders_world_map' into development
2 parents dcd4d24 + 4ff669f commit 276e79f

10 files changed

Lines changed: 813 additions & 30 deletions

File tree

frontend/package-lock.json

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
"react-app/jest"
3838
]
3939
},
40+
"jest": {
41+
"moduleNameMapper": {
42+
"\\.(css|less|scss|sass)$": "identity-obj-proxy"
43+
}
44+
},
4045
"browserslist": {
4146
"production": [
4247
">0.2%",

frontend/src/_test_/DisclaimerImpressumPrivacy.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import { render, screen } from '@testing-library/react';
33
import '@testing-library/jest-dom';
44
import { MemoryRouter } from 'react-router-dom';
5-
import { LandingPageLight } from '../pages/light_mode/landing_light';
5+
import { LandingPageLight } from '../pages/home';
66

77
describe('LandingPageLight', () => {
88
test('renders the DisclaimerBox, PrivacyPolicyBox, and ImpressumBox', () => {

frontend/src/_test_/SearchPage.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
33
import { MemoryRouter } from 'react-router-dom';
44
import '@testing-library/jest-dom';
5-
import SearchPageLight from '../pages/light_mode/search_light';
5+
import SearchPageLight from '../pages/search';
66

77
// Mock the global fetch function
88
global.fetch = jest.fn();
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import {
2+
animate,
3+
motion,
4+
useMotionValue,
5+
useMotionValueEvent,
6+
useTransform,
7+
} from "framer-motion";
8+
import { useEffect, useRef, useState } from "react";
9+
import styles from './Slider.module.css';
10+
11+
const MAX_OVERFLOW = 50;
12+
13+
export default function ElasticSlider({
14+
defaultValue = 50,
15+
startingValue = 0,
16+
maxValue = 100,
17+
className = "",
18+
isStepped = false,
19+
stepSize = 1,
20+
leftIcon = <span></span>,
21+
rightIcon = <span>+</span>,
22+
onChange = null,
23+
}) {
24+
return (
25+
<div className={`${styles.sliderContainer} ${className}`}>
26+
<Slider
27+
defaultValue={defaultValue}
28+
startingValue={startingValue}
29+
maxValue={maxValue}
30+
isStepped={isStepped}
31+
stepSize={stepSize}
32+
leftIcon={leftIcon}
33+
rightIcon={rightIcon}
34+
onChange={onChange}
35+
/>
36+
</div>
37+
);
38+
}
39+
40+
function Slider({
41+
defaultValue,
42+
startingValue,
43+
maxValue,
44+
isStepped,
45+
stepSize,
46+
leftIcon,
47+
rightIcon,
48+
onChange,
49+
}) {
50+
const [value, setValue] = useState(defaultValue);
51+
const sliderRef = useRef(null);
52+
const [region, setRegion] = useState("middle");
53+
const clientX = useMotionValue(0);
54+
const overflow = useMotionValue(0);
55+
const scale = useMotionValue(1);
56+
57+
useEffect(() => {
58+
setValue(defaultValue);
59+
}, [defaultValue]);
60+
61+
// Call onChange when value changes
62+
useEffect(() => {
63+
if (onChange && typeof onChange === 'function') {
64+
onChange(value);
65+
}
66+
}, [value, onChange]);
67+
68+
useMotionValueEvent(clientX, "change", (latest) => {
69+
if (sliderRef.current) {
70+
const { left, right } = sliderRef.current.getBoundingClientRect();
71+
let newValue;
72+
73+
if (latest < left) {
74+
setRegion("left");
75+
newValue = left - latest;
76+
} else if (latest > right) {
77+
setRegion("right");
78+
newValue = latest - right;
79+
} else {
80+
setRegion("middle");
81+
newValue = 0;
82+
}
83+
84+
overflow.jump(decay(newValue, MAX_OVERFLOW));
85+
}
86+
});
87+
88+
const handlePointerMove = (e) => {
89+
if (e.buttons > 0 && sliderRef.current) {
90+
const { left, width } = sliderRef.current.getBoundingClientRect();
91+
let newValue = startingValue + ((e.clientX - left) / width) * (maxValue - startingValue);
92+
93+
if (isStepped) {
94+
newValue = Math.round(newValue / stepSize) * stepSize;
95+
}
96+
97+
newValue = Math.min(Math.max(newValue, startingValue), maxValue);
98+
setValue(newValue);
99+
clientX.jump(e.clientX);
100+
}
101+
};
102+
103+
const handlePointerDown = (e) => {
104+
handlePointerMove(e);
105+
e.currentTarget.setPointerCapture(e.pointerId);
106+
};
107+
108+
const handlePointerUp = () => {
109+
animate(overflow, 0, { type: "spring", bounce: 0.5 });
110+
};
111+
112+
const getRangePercentage = () => {
113+
const totalRange = maxValue - startingValue;
114+
if (totalRange === 0) return 0;
115+
116+
return ((value - startingValue) / totalRange) * 100;
117+
};
118+
119+
return (
120+
<>
121+
<motion.div
122+
onHoverStart={() => animate(scale, 1.2)}
123+
onHoverEnd={() => animate(scale, 1)}
124+
onTouchStart={() => animate(scale, 1.2)}
125+
onTouchEnd={() => animate(scale, 1)}
126+
style={{
127+
scale,
128+
opacity: useTransform(scale, [1, 1.2], [0.7, 1]),
129+
}}
130+
className={styles.sliderWrapper}
131+
>
132+
<motion.div
133+
animate={{
134+
scale: region === "left" ? [1, 1.4, 1] : 1,
135+
transition: { duration: 0.25 },
136+
}}
137+
style={{
138+
x: useTransform(() =>
139+
region === "left" ? -overflow.get() / scale.get() : 0,
140+
),
141+
}}
142+
className={styles.icon}
143+
>
144+
{leftIcon}
145+
</motion.div>
146+
147+
<div
148+
ref={sliderRef}
149+
className={styles.sliderRoot}
150+
onPointerMove={handlePointerMove}
151+
onPointerDown={handlePointerDown}
152+
onPointerUp={handlePointerUp}
153+
>
154+
<motion.div
155+
style={{
156+
scaleX: useTransform(() => {
157+
if (sliderRef.current) {
158+
const { width } = sliderRef.current.getBoundingClientRect();
159+
return 1 + overflow.get() / width;
160+
}
161+
}),
162+
scaleY: useTransform(overflow, [0, MAX_OVERFLOW], [1, 0.8]),
163+
transformOrigin: useTransform(() => {
164+
if (sliderRef.current) {
165+
const { left, width } = sliderRef.current.getBoundingClientRect();
166+
return clientX.get() < left + width / 2 ? "right" : "left";
167+
}
168+
}),
169+
height: useTransform(scale, [1, 1.2], [6, 12]),
170+
marginTop: useTransform(scale, [1, 1.2], [0, -3]),
171+
marginBottom: useTransform(scale, [1, 1.2], [0, -3]),
172+
}}
173+
className={styles.sliderTrackWrapper}
174+
>
175+
<div className={styles.sliderTrack}>
176+
<div
177+
className={styles.sliderRange}
178+
style={{ width: `${getRangePercentage()}%` }}
179+
/>
180+
</div>
181+
</motion.div>
182+
</div>
183+
184+
<motion.div
185+
animate={{
186+
scale: region === "right" ? [1, 1.4, 1] : 1,
187+
transition: { duration: 0.25 },
188+
}}
189+
style={{
190+
x: useTransform(() =>
191+
region === "right" ? overflow.get() / scale.get() : 0,
192+
),
193+
}}
194+
className={styles.icon}
195+
>
196+
{rightIcon}
197+
</motion.div>
198+
</motion.div>
199+
</>
200+
);
201+
}
202+
203+
function decay(value, max) {
204+
if (max === 0) {
205+
return 0;
206+
}
207+
208+
const entry = value / max;
209+
const sigmoid = 2 * (1 / (1 + Math.exp(-entry)) - 0.5);
210+
211+
return sigmoid * max;
212+
}

0 commit comments

Comments
 (0)