related-ui-components 1.8.5 → 1.8.6

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.
@@ -1,9 +1,4 @@
1
- import React, {
2
- useState,
3
- useRef,
4
- useEffect,
5
- useMemo, // Added useMemo for wheelPaths
6
- } from "react";
1
+ import React, { useState, useRef, useEffect } from "react";
7
2
  import {
8
3
  View,
9
4
  StyleSheet,
@@ -20,37 +15,15 @@ export interface SpinWheelItem {
20
15
  id: string | number;
21
16
  label: string;
22
17
  value?: any;
23
- color: string; // Expect color to be provided, or handle default more explicitly
18
+ color: string;
24
19
  textColor?: string;
25
20
  }
26
21
 
27
- // Default random colors (can be used if item.color is not provided)
28
- const defaultColors = [
29
- "#FF0000",
30
- "#FFA500",
31
- "#FFFF00",
32
- "#008000",
33
- "#0000FF",
34
- "#800080",
35
- "#FFC0CB",
36
- "#00FFFF",
37
- "#FF00FF",
38
- "#00FF00",
39
- "#4B0082",
40
- "#EE82EE",
41
- "#40E0D0",
42
- "#FFD700",
43
- "#C0C0C0",
44
- "#FFDAB9",
45
- "#E6E6FA",
46
- "#008080",
47
- "#FF7F50",
48
- "#DC143C",
49
- "#87CEEB",
50
- "#7FFF00",
51
- "#CCCCFF",
52
- "#FF6347",
53
- "#FA8072",
22
+ const colors = [
23
+ "#FF0000", "#FFA500", "#FFFF00", "#008000", "#0000FF", "#800080",
24
+ "#FFC0CB", "#00FFFF", "#FF00FF", "#00FF00", "#4B0082", "#EE82EE",
25
+ "#40E0D0", "#FFD700", "#C0C0C0", "#FFDAB9", "#E6E6FA", "#008080",
26
+ "#FF7F50", "#DC143C", "#87CEEB", "#7FFF00", "#CCCCFF", "#FF6347", "#FA8072"
54
27
  ];
55
28
 
56
29
  interface SpinWheelProps {
@@ -70,7 +43,7 @@ interface SpinWheelProps {
70
43
  wheelTextColor?: string;
71
44
  knobColor?: string;
72
45
  centerComponent?: React.ReactNode;
73
- predeterminedWinnerId?: string | number | null; // New prop
46
+ winner?: SpinWheelItem | null; // Winner to spin to
74
47
  }
75
48
 
76
49
  const SpinWheel: React.FC<SpinWheelProps> = ({
@@ -89,33 +62,27 @@ const SpinWheel: React.FC<SpinWheelProps> = ({
89
62
  actionButtonStyle,
90
63
  actionButtonTextStyle,
91
64
  wheelTextColor = "#FFFFFF",
92
- centerComponent,
93
- predeterminedWinnerId = null, // Default to null
65
+ winner,
94
66
  }) => {
95
- const wheelItems = useMemo(
96
- () => (items && items.length > 0 ? items : []),
97
- [items]
98
- );
99
-
67
+ const wheelItems = items.length > 0 ? items : [];
100
68
  const [spinning, setSpinning] = useState(false);
101
- const [_winnerState, setWinnerState] = useState<SpinWheelItem | null>(null); // Renamed to avoid conflict
69
+ const [internalWinner, setInternalWinner] = useState<SpinWheelItem | null>(null);
102
70
  const rotateValue = useRef(new Animated.Value(0)).current;
103
- const rotationRef = useRef(0); // Tracks cumulative rotation in "number of turns"
71
+ const rotationRef = useRef(0);
104
72
 
105
73
  useEffect(() => {
106
- const listenerId = rotateValue.addListener(({ value }) => {
74
+ const listener = rotateValue.addListener(({ value }) => {
107
75
  rotationRef.current = value;
108
76
  });
109
77
  return () => {
110
- rotateValue.removeListener(listenerId);
78
+ rotateValue.removeListener(listener);
111
79
  };
112
80
  }, [rotateValue]);
113
81
 
114
- const anglePerItem =
115
- wheelItems.length > 0 ? 360 / wheelItems.length : 0;
82
+ const anglePerItem = 360 / wheelItems.length;
116
83
 
117
- const wheelPaths = useMemo(() => {
118
- if (wheelItems.length === 0) return [];
84
+ // Generate wheel paths and text positions
85
+ const generateWheelPaths = () => {
119
86
  return wheelItems.map((item, index) => {
120
87
  const startAngle = index * anglePerItem;
121
88
  const endAngle = (index + 1) * anglePerItem;
@@ -125,15 +92,14 @@ const SpinWheel: React.FC<SpinWheelProps> = ({
125
92
  const y1 = size / 2 + (size / 2) * Math.sin(startRad);
126
93
  const x2 = size / 2 + (size / 2) * Math.cos(endRad);
127
94
  const y2 = size / 2 + (size / 2) * Math.sin(endRad);
128
- const largeArcFlag = anglePerItem <= 180 ? "0" : "1";
95
+ const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
129
96
  const pathData = [
130
97
  `M ${size / 2} ${size / 2}`,
131
98
  `L ${x1} ${y1}`,
132
99
  `A ${size / 2} ${size / 2} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
133
100
  "Z",
134
101
  ].join(" ");
135
- const midAngle = startAngle + anglePerItem / 2;
136
- const midRad = (midAngle * Math.PI) / 180;
102
+ const midRad = ((startAngle + endAngle) / 2) * (Math.PI / 180);
137
103
  const textX = size / 2 + size * 0.32 * Math.cos(midRad);
138
104
  const textY = size / 2 + size * 0.32 * Math.sin(midRad);
139
105
  return {
@@ -141,136 +107,75 @@ const SpinWheel: React.FC<SpinWheelProps> = ({
141
107
  item,
142
108
  textX,
143
109
  textY,
144
- angle: midAngle,
145
- color: item.color || defaultColors[index % defaultColors.length],
110
+ angle: (startAngle + endAngle) / 2,
146
111
  };
147
112
  });
148
- }, [wheelItems, anglePerItem, size]);
113
+ };
149
114
 
150
- const handleSpin = () => {
151
- if (spinning || !enabled || wheelItems.length === 0 || anglePerItem === 0) {
152
- return;
153
- }
115
+ const wheelPaths = generateWheelPaths();
116
+
117
+ // Helper: get index of a SpinWheelItem
118
+ const getItemIndex = (target: SpinWheelItem) =>
119
+ wheelItems.findIndex((item) => item.id === target.id);
154
120
 
121
+ // Helper: spin to a given index
122
+ const spinToIndex = (targetIndex: number, callbackItem: SpinWheelItem) => {
155
123
  setSpinning(true);
156
124
  onSpinStart?.();
157
125
 
158
- let targetAnimationToValue: number;
159
- let winnerToAnnounce: SpinWheelItem | null = null;
160
-
161
- const currentWinnerId = predeterminedWinnerId; // Capture prop value at spin start
162
- let targetWinnerItem: SpinWheelItem | null = null;
163
-
164
- if (currentWinnerId != null) {
165
- const foundItem = wheelItems.find((item) => item.id === currentWinnerId);
166
- if (foundItem) {
167
- targetWinnerItem = foundItem;
168
- } else {
169
- console.warn(
170
- `SpinWheel: Predetermined winner with id "${currentWinnerId}" not found. Spinning randomly.`
171
- );
172
- }
173
- }
174
-
175
- if (targetWinnerItem) {
176
- winnerToAnnounce = targetWinnerItem;
177
- const winnerIndex = wheelItems.indexOf(targetWinnerItem);
178
-
179
- // Calculate the middle angle of the target segment within the wheel's coordinate system
180
- const targetSegmentMidAngle_deg = (winnerIndex + 0.5) * anglePerItem;
181
-
182
- // The pointer is at 270 degrees (top). We want the targetSegmentMidAngle_deg
183
- // to align with this pointer.
184
- // If the wheel rotates by R, a point A on wheel is at (A+R)%360.
185
- // So, (targetSegmentMidAngle_deg + R) % 360 = 270.
186
- // R % 360 = (270 - targetSegmentMidAngle_deg + 360*k) % 360.
187
- // This R % 360 is the target orientation for the wheel's 0-degree mark.
188
- const targetWheelOrientation_deg =
189
- (270 - targetSegmentMidAngle_deg + 360 * 10) % 360; // *10 to ensure positive
190
-
191
- const currentWheelOrientation_deg = (rotationRef.current * 360) % 360;
126
+ // The pointer is at 270deg (top), so we want the winner to land there.
127
+ // The center of the segment is at: (index + 0.5) * anglePerItem
128
+ const winnerAngle =
129
+ 360 - ((targetIndex + 0.5) * anglePerItem - 270);
192
130
 
193
- // Additional rotation needed to reach the target orientation (shortest positive path)
194
- const rotationToTarget_deg =
195
- (targetWheelOrientation_deg - currentWheelOrientation_deg + 360) % 360;
196
-
197
- const numFullRotations = 3 + Math.floor(Math.random() * 3); // 3 to 5 full spins
198
- const totalAdditionalRotation_deg =
199
- numFullRotations * 360 + rotationToTarget_deg;
200
-
201
- targetAnimationToValue =
202
- rotationRef.current + totalAdditionalRotation_deg / 360;
203
- } else {
204
- // Fallback to original random spin logic
205
- const randomSpins = 3 + Math.random() * 2; // 3-5 full rotations
206
- const randomAngleOffset = Math.random() * 360;
207
- const totalRandomRotation_deg = 360 * randomSpins + randomAngleOffset;
208
- targetAnimationToValue =
209
- rotationRef.current + totalRandomRotation_deg / 360;
210
- }
131
+ // Add extra spins for effect
132
+ const extraSpins = 3;
133
+ const currentRotation = rotationRef.current * 360;
134
+ const targetRotation = currentRotation + extraSpins * 360 + winnerAngle;
211
135
 
212
136
  Animated.timing(rotateValue, {
213
- toValue: targetAnimationToValue,
137
+ toValue: targetRotation / 360,
214
138
  duration: spinDuration,
215
139
  easing: Easing.out(Easing.cubic),
216
140
  useNativeDriver: true,
217
141
  }).start(() => {
218
142
  setSpinning(false);
219
-
220
- if (winnerToAnnounce) {
221
- setWinnerState(winnerToAnnounce);
222
- onSpinEnd?.(winnerToAnnounce);
223
- } else {
224
- // Calculate winner from random spin based on final rotation
225
- const finalRotationDegrees = rotationRef.current * 360;
226
- const pointerFixedAt_deg = 270; // Pointer is at the top
227
-
228
- // Determine which original angle on the wheel is now under the pointer
229
- const angleUnderPointer_orig =
230
- (pointerFixedAt_deg - finalRotationDegrees + 360 * 10) % 360; // *10 for positive
231
-
232
- let winningSegmentIndex = Math.floor(
233
- angleUnderPointer_orig / anglePerItem
234
- );
235
- // Ensure index is within bounds
236
- winningSegmentIndex = Math.max(
237
- 0,
238
- Math.min(wheelItems.length - 1, winningSegmentIndex)
239
- );
240
-
241
- const calculatedWinner = wheelItems[winningSegmentIndex];
242
-
243
- if (calculatedWinner) {
244
- setWinnerState(calculatedWinner);
245
- onSpinEnd?.(calculatedWinner);
246
- } else if (wheelItems.length > 0) {
247
- // Fallback, should not happen if items exist and calculation is correct
248
- console.error(
249
- "SpinWheel: Could not determine winner from random spin. Falling back to first item."
250
- );
251
- setWinnerState(wheelItems[0]);
252
- onSpinEnd?.(wheelItems[0]);
253
- }
254
- }
143
+ setInternalWinner(callbackItem);
144
+ onSpinEnd?.(callbackItem);
255
145
  });
256
146
  };
257
147
 
258
- const rotateStyle = rotateValue.interpolate({
148
+ // If winner prop changes, spin to it
149
+ useEffect(() => {
150
+ if (!winner || spinning) return;
151
+ const winnerIndex = getItemIndex(winner);
152
+ if (winnerIndex === -1) return;
153
+ spinToIndex(winnerIndex, winner);
154
+ // eslint-disable-next-line react-hooks/exhaustive-deps
155
+ }, [winner]);
156
+
157
+ // Handle random spin (if no winner is set)
158
+ const handleSpin = () => {
159
+ if (spinning || !enabled) return;
160
+ // If winner prop is set, ignore button (or you can allow override)
161
+ if (winner) return;
162
+
163
+ // Pick a random winner
164
+ const randomIndex = Math.floor(Math.random() * wheelItems.length);
165
+ const randomWinner = wheelItems[randomIndex];
166
+ spinToIndex(randomIndex, randomWinner);
167
+ };
168
+
169
+ // Animation interpolation for rotation
170
+ const rotate = rotateValue.interpolate({
259
171
  inputRange: [0, 1],
260
172
  outputRange: ["0deg", "360deg"],
261
173
  });
262
174
 
263
- if (wheelItems.length === 0) {
264
- return (
265
- <View style={[styles.container, containerStyle, { height: size }]}>
266
- <Text>No items to display in the wheel.</Text>
267
- </View>
268
- );
269
- }
270
-
271
175
  return (
272
176
  <View style={[styles.container, containerStyle]}>
273
- <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
177
+ <View style={{ width: size, height: size }}>
178
+ {/* The wheel */}
274
179
  <Animated.View
275
180
  style={[
276
181
  styles.wheelContainer,
@@ -278,66 +183,96 @@ const SpinWheel: React.FC<SpinWheelProps> = ({
278
183
  width: size,
279
184
  height: size,
280
185
  borderRadius: size / 2,
281
- transform: [{ rotate: rotateStyle }],
186
+ transform: [{ rotate }],
282
187
  },
283
188
  ]}
284
189
  >
285
190
  <Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
286
191
  <G>
287
- {wheelPaths.map(({ path, item, textX, textY, angle, color }) => (
288
- <React.Fragment key={item.id}>
289
- <Path d={path} fill={color} stroke="#000000" strokeWidth={0.5} />
290
- <SvgText
291
- x={textX}
292
- y={textY}
293
- fill={item.textColor || wheelTextColor}
294
- fontSize={wheelTextStyle?.fontSize || Math.max(8, size / 25)} // Dynamic font size
295
- fontWeight={wheelTextStyle?.fontWeight || "bold"}
296
- textAnchor="middle"
297
- alignmentBaseline="central"
298
- transform={`rotate(${angle + 90}, ${textX}, ${textY} )`} // Adjusted rotation for readability
299
- >
300
- {item.label}
301
- </SvgText>
302
- </React.Fragment>
303
- ))}
192
+ {wheelPaths.map(
193
+ ({ path, item, textX, textY, angle }, index) => (
194
+ <React.Fragment key={item.id}>
195
+ <Path
196
+ d={path}
197
+ fill={
198
+ !item.color
199
+ ? colors[index % colors.length]
200
+ : item.color
201
+ }
202
+ stroke="#000000"
203
+ strokeWidth={1}
204
+ />
205
+ <SvgText
206
+ x={textX}
207
+ y={textY}
208
+ fill={item.textColor || wheelTextColor}
209
+ fontSize={wheelTextStyle?.fontSize || 14}
210
+ fontWeight={wheelTextStyle?.fontWeight || "bold"}
211
+ textAnchor="middle"
212
+ alignmentBaseline="central"
213
+ transform={`rotate(${angle + 180}, ${textX}, ${textY})`}
214
+ >
215
+ {item.label}
216
+ </SvgText>
217
+ </React.Fragment>
218
+ )
219
+ )}
304
220
  </G>
305
221
  </Svg>
306
222
  </Animated.View>
307
223
 
308
- {centerComponent ? (
309
- <View style={styles.centerComponentWrapper}>{centerComponent}</View>
310
- ) : (
224
+ {/* The center circle */}
225
+ <View
226
+ style={[
227
+ styles.wheelCenter,
228
+ {
229
+ width: size / 5,
230
+ height: size / 5,
231
+ transform: [
232
+ { translateX: -size / 10 },
233
+ { translateY: -size / 10 },
234
+ ],
235
+ borderRadius: size / 5,
236
+ },
237
+ centerStyle,
238
+ ]}
239
+ >
240
+ {/** Optional: custom center component */}
241
+ </View>
242
+
243
+ {/* The pointer is a triangle on top */}
244
+ <View style={styles.pointerPosition}>
311
245
  <View
312
246
  style={[
313
- styles.wheelCenter,
314
- {
315
- width: size / 5,
316
- height: size / 5,
317
- borderRadius: size / 10, // Should be half of width/height
318
- // Centering is now handled by absolute positioning from parent
319
- },
320
- centerStyle,
247
+ styles.pointer,
248
+ { borderBottomColor: knobColor },
249
+ knobStyle,
321
250
  ]}
322
251
  />
323
- )}
252
+ </View>
324
253
 
325
- <View style={styles.pointerContainer}>
326
- <View
327
- style={[styles.pointer, { borderBottomColor: knobColor }, knobStyle]}
328
- />
254
+ {/* Action Button */}
255
+ <View
256
+ style={{
257
+ position: "absolute",
258
+ width: "100%",
259
+ alignItems: "center",
260
+ justifyContent: "center",
261
+ bottom: -70,
262
+ zIndex: 2,
263
+ }}
264
+ >
265
+ <TouchableOpacity
266
+ onPress={handleSpin}
267
+ disabled={spinning || !enabled || !!winner}
268
+ style={[styles.actionButton, actionButtonStyle]}
269
+ >
270
+ <Text style={[styles.actionButtonText, actionButtonTextStyle]}>
271
+ {spinButtonText}
272
+ </Text>
273
+ </TouchableOpacity>
329
274
  </View>
330
275
  </View>
331
-
332
- <TouchableOpacity
333
- onPress={handleSpin}
334
- disabled={spinning || !enabled}
335
- style={[styles.actionButton, actionButtonStyle]}
336
- >
337
- <Text style={[styles.actionButtonText, actionButtonTextStyle]}>
338
- {spinButtonText}
339
- </Text>
340
- </TouchableOpacity>
341
276
  </View>
342
277
  );
343
278
  };
@@ -346,58 +281,49 @@ const styles = StyleSheet.create({
346
281
  container: {
347
282
  alignItems: "center",
348
283
  justifyContent: "center",
349
- paddingVertical: 20, // Added padding
284
+ marginTop: 20,
285
+ marginBottom: 70,
350
286
  },
351
287
  wheelContainer: {
352
288
  overflow: "hidden",
353
- backgroundColor: "transparent", // Ensure SVG is visible
354
- alignItems: "center",
355
- justifyContent: "center",
356
- },
357
- centerComponentWrapper: { // For custom center component
358
- position: 'absolute',
359
- zIndex: 2,
289
+ backgroundColor: "transparent",
360
290
  },
361
- wheelCenter: { // Default center circle
291
+ wheelCenter: {
362
292
  position: "absolute",
363
- backgroundColor: "#333333", // Darker center
364
- borderWidth: 2,
365
- borderColor: "#FFFFFF", // White border for center
366
- zIndex: 2, // Above wheel paths, below pointer
293
+ top: "50%",
294
+ left: "50%",
295
+ backgroundColor: "#000000",
296
+ borderWidth: 1,
297
+ borderColor: "#333333",
298
+ zIndex: 1,
367
299
  },
368
- pointerContainer: { // Position the pointer correctly at the top center
300
+ pointerPosition: {
369
301
  position: "absolute",
370
- top: -5, // Adjust to make pointer sit nicely on the edge
371
302
  left: "50%",
372
- transform: [{ translateX: -10 }], // Half of pointer base width
373
- zIndex: 3, // Above everything else on the wheel
303
+ transform: [{ translateX: -10 }, { rotate: "180deg" }],
304
+ zIndex: 2,
374
305
  },
375
306
  pointer: {
376
307
  width: 0,
377
308
  height: 0,
378
309
  backgroundColor: "transparent",
379
310
  borderStyle: "solid",
380
- borderLeftWidth: 10, // Base of triangle
381
- borderRightWidth: 10, // Base of triangle
382
- borderBottomWidth: 20, // Height of triangle
311
+ borderLeftWidth: 10,
312
+ borderRightWidth: 10,
313
+ borderBottomWidth: 15,
383
314
  borderLeftColor: "transparent",
384
315
  borderRightColor: "transparent",
385
- // borderBottomColor is set by knobColor prop
316
+ borderBottomColor: "#D81E5B",
386
317
  },
387
318
  actionButton: {
388
- marginTop: 30, // Space between wheel and button
389
319
  paddingHorizontal: 30,
390
320
  paddingVertical: 12,
391
321
  borderRadius: 25,
392
- backgroundColor: "#4CAF50", // Example button color
393
322
  shadowColor: "#000",
394
- shadowOffset: { width: 0, height: 2 },
395
- shadowOpacity: 0.25,
396
- shadowRadius: 3.84,
397
- elevation: 5,
323
+ shadowRadius: 3,
324
+ backgroundColor: "grey",
398
325
  },
399
326
  actionButtonText: {
400
- color: "#FFFFFF",
401
327
  fontWeight: "bold",
402
328
  fontSize: 16,
403
329
  },