Skip to content

Commit 66c6b4b

Browse files
authored
Redesigned ProductDetails.tsx for new UI layout (#960)
* Redesigned ProductDetails.tsx for new UI layout * Created ScoreGauge.tsx and updated ProductDetails.tsx * Created ReadMoreArea.tsx for a manufacturer description * Added span to score rating section
1 parent 566bbfe commit 66c6b4b

7 files changed

Lines changed: 411 additions & 79 deletions

File tree

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"react-addons-css-transition-group": "^15.6.2",
8686
"react-dom": "^18.2.0",
8787
"react-helmet": "^6.1.0",
88+
"react-icons": "^5.5.0",
8889
"react-loader-spinner": "^5.1.5",
8990
"react-onclickoutside": "^6.12.2",
9091
"react-paginate": "^7.1.3",

src/assets/info.results.png

15.1 KB
Loading

src/components/ReadMoreArea.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react';
2+
import styled from 'styled-components';
3+
4+
const Container = styled.div`
5+
display: block;
6+
.text {
7+
display: inline;
8+
}
9+
.toggle {
10+
display: inline-block;
11+
margin-left: 0.5em;
12+
color: #d33333;
13+
cursor: pointer;
14+
font-weight: 600;
15+
}
16+
`;
17+
18+
interface IReadMoreArea {
19+
text: string;
20+
maxLength?: number;
21+
moreLabel?: string;
22+
lessLabel?: string;
23+
}
24+
25+
export const ReadMoreArea: React.FC<IReadMoreArea> = ({
26+
text,
27+
maxLength = 250,
28+
moreLabel = 'Czytaj więcej',
29+
lessLabel = 'Pokaż mniej',
30+
}) => {
31+
const [expanded, setExpanded] = React.useState(false);
32+
33+
if (!text) return null;
34+
35+
const isLong = text.length > maxLength;
36+
const shown = !isLong || expanded ? text : text.slice(0, maxLength).trimEnd() + '…';
37+
38+
return (
39+
<Container>
40+
<span className="text">{shown}</span>
41+
{isLong && (
42+
<span className="toggle" onClick={() => setExpanded((s) => !s)} role="button" tabIndex={0} onKeyDown={() => setExpanded((s) => !s)}>
43+
{expanded ? lessLabel : moreLabel}
44+
</span>
45+
)}
46+
</Container>
47+
);
48+
};
49+
50+
export default ReadMoreArea;

src/components/ScoreBar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ const BarComponent = styled.div`
2929

3030
const ValueBar = styled(BarComponent)<{ value?: number; animation?: IAnimation }>`
3131
text-align: right;
32+
position: relative;
33+
border-radius: 10px;
34+
overflow: hidden;
3235
3336
.value-belt {
3437
position: absolute;
@@ -78,7 +81,6 @@ export const ScoreBar: React.FC<IScoreBar> = ({ value, unit, animation, missingV
7881
return (
7982
<ValueBar value={knownValue} animation={animation}>
8083
<div className="value-belt" data-testid="value-belt" />
81-
<div className="label">{scoreText}</div>
8284
</ValueBar>
8385
);
8486
} else {

src/components/ScoreGauge.tsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import React from "react";
2+
import styled, { keyframes } from "styled-components";
3+
import { lighten } from "polished";
4+
5+
import { seconds } from "app/generics";
6+
import { color, fontSize } from "@Styles/theme";
7+
8+
export interface IAnimation {
9+
duration: seconds;
10+
delay?: seconds;
11+
iterations?: number;
12+
}
13+
14+
interface IScoreGauge {
15+
value?: number | null;
16+
unit?: string;
17+
animation?: IAnimation;
18+
missingValuePlaceholder?: number | string;
19+
}
20+
21+
// PARAMETRY
22+
const SIZE = 120;
23+
const STROKE = 12;
24+
const CX = SIZE / 2;
25+
const CY = SIZE / 2; // przesunięcie w dół, aby wyśrodkować wizualnie
26+
const R = (SIZE - STROKE) / 2;
27+
28+
// Łuk 270° od -135° do +135°
29+
const startAngle = (225 * Math.PI) / 180;
30+
const endAngle = (-45 * Math.PI) / 180;
31+
32+
const arcLength = R * (Math.abs(endAngle - startAngle)); // pełne 270° łuku
33+
34+
// GENERATOR ŁUKU
35+
const describeArc = () => {
36+
const x1 = CX + R * Math.cos(startAngle);
37+
const y1 = CY + R * Math.sin(startAngle);
38+
const x2 = CX + R * Math.cos(endAngle);
39+
const y2 = CY + R * Math.sin(endAngle);
40+
41+
return `M ${x1} ${y1} A ${R} ${R} 0 1 1 ${x2} ${y2}`;
42+
};
43+
44+
// ANIMACJA
45+
const animateArc = (value: number) => keyframes`
46+
from { stroke-dashoffset: ${arcLength}; }
47+
to { stroke-dashoffset: ${(1 - value / 100) * arcLength}; }
48+
`;
49+
50+
51+
const GaugeWrapper = styled.div`
52+
width: ${SIZE}px;
53+
height: ${SIZE}px;
54+
position: relative;
55+
display: flex;
56+
align-items: center;
57+
justify-content: center;
58+
59+
.label {
60+
position: absolute;
61+
font-size: 19px;
62+
font-weight: bold;
63+
z-index: 2;
64+
text-align: center;
65+
top: 32px;
66+
}
67+
68+
.subtitle {
69+
position: absolute;
70+
top: 86px;
71+
width: 100%;
72+
text-align: center;
73+
font-size: 12px;
74+
color: ${color.text ? color.text.primary : "#000"};
75+
z-index: 2;
76+
}
77+
`;
78+
79+
const TrackPath = styled.path`
80+
stroke: ${color.background.primary};
81+
stroke-width: ${STROKE};
82+
fill: none;
83+
opacity: 0.2;
84+
stroke-linecap: round;
85+
`;
86+
87+
88+
const ValuePath = styled.path<{ value: number; animation?: IAnimation }>`
89+
stroke: ${lighten(0.1)(color.background.red)};
90+
stroke-width: ${STROKE};
91+
fill: none;
92+
stroke-linecap: round;
93+
94+
stroke-dasharray: ${arcLength};
95+
stroke-dashoffset: ${({ value }) => (1 - value / 100) * arcLength};
96+
97+
animation-name: ${({ value }) => animateArc(value)};
98+
animation-duration: ${({ animation }) => (animation?.duration || 0) + "s"};
99+
animation-delay: ${({ animation }) => (animation?.delay || 0) + "s"};
100+
animation-timing-function: ease-out;
101+
animation-fill-mode: forwards;
102+
`;
103+
104+
export const ScoreGauge: React.FC<IScoreGauge> = ({
105+
value,
106+
unit,
107+
animation,
108+
missingValuePlaceholder,
109+
}) => {
110+
if (value === undefined) {
111+
return (
112+
<GaugeWrapper>
113+
<div className="label">{missingValuePlaceholder}</div>
114+
</GaugeWrapper>
115+
);
116+
}
117+
118+
const safeValue = value ?? 0;
119+
const label = unit ? `${safeValue} ${unit}` : safeValue.toString();
120+
121+
const arc = describeArc();
122+
123+
return (
124+
<GaugeWrapper>
125+
<svg width={SIZE} height={SIZE} viewBox={`0 -${SIZE * 0.65} ${SIZE} ${SIZE * 1.22}`}>
126+
<TrackPath d={arc} />
127+
<ValuePath d={arc} value={safeValue} animation={animation} />
128+
</svg>
129+
130+
<div className="label">{label}</div>
131+
<div className="subtitle">Polski kapitał</div>
132+
</GaugeWrapper>
133+
);
134+
};

0 commit comments

Comments
 (0)