@campxdev/react-native-blueprint 0.1.0
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/LICENSE +20 -0
- package/README.md +358 -0
- package/lib/module/app/_layout.js +23 -0
- package/lib/module/app/_layout.js.map +1 -0
- package/lib/module/assets/icons/weather_icons/drizzle.png +0 -0
- package/lib/module/assets/icons/weather_icons/foggy.png +0 -0
- package/lib/module/assets/icons/weather_icons/freezing_rain.png +0 -0
- package/lib/module/assets/icons/weather_icons/partly_cloudy.png +0 -0
- package/lib/module/assets/icons/weather_icons/rainy.png +0 -0
- package/lib/module/assets/icons/weather_icons/showers.png +0 -0
- package/lib/module/assets/icons/weather_icons/sunny_weather.png +0 -0
- package/lib/module/assets/icons/weather_icons/thunderstorm.png +0 -0
- package/lib/module/assets/icons/weather_icons/thunderstorm_hail.png +0 -0
- package/lib/module/components/theme-config.js +265 -0
- package/lib/module/components/theme-config.js.map +1 -0
- package/lib/module/components/ui/Accordion.js +228 -0
- package/lib/module/components/ui/Accordion.js.map +1 -0
- package/lib/module/components/ui/Alert-Dialog.js +266 -0
- package/lib/module/components/ui/Alert-Dialog.js.map +1 -0
- package/lib/module/components/ui/Alert.js +107 -0
- package/lib/module/components/ui/Alert.js.map +1 -0
- package/lib/module/components/ui/AppBar.js +403 -0
- package/lib/module/components/ui/AppBar.js.map +1 -0
- package/lib/module/components/ui/Aspect-Ratio.js +27 -0
- package/lib/module/components/ui/Aspect-Ratio.js.map +1 -0
- package/lib/module/components/ui/Avatar.js +97 -0
- package/lib/module/components/ui/Avatar.js.map +1 -0
- package/lib/module/components/ui/Badge.js +127 -0
- package/lib/module/components/ui/Badge.js.map +1 -0
- package/lib/module/components/ui/Bottom-Sheet.js +144 -0
- package/lib/module/components/ui/Bottom-Sheet.js.map +1 -0
- package/lib/module/components/ui/Button.js +88 -0
- package/lib/module/components/ui/Button.js.map +1 -0
- package/lib/module/components/ui/Card.js +176 -0
- package/lib/module/components/ui/Card.js.map +1 -0
- package/lib/module/components/ui/Checkbox.js +65 -0
- package/lib/module/components/ui/Checkbox.js.map +1 -0
- package/lib/module/components/ui/Collapsible.js +42 -0
- package/lib/module/components/ui/Collapsible.js.map +1 -0
- package/lib/module/components/ui/Context-Menu.js +287 -0
- package/lib/module/components/ui/Context-Menu.js.map +1 -0
- package/lib/module/components/ui/Custom-Card.js +202 -0
- package/lib/module/components/ui/Custom-Card.js.map +1 -0
- package/lib/module/components/ui/Dialog.js +202 -0
- package/lib/module/components/ui/Dialog.js.map +1 -0
- package/lib/module/components/ui/Dropdown-Menu.js +421 -0
- package/lib/module/components/ui/Dropdown-Menu.js.map +1 -0
- package/lib/module/components/ui/Floating-Action.js +50 -0
- package/lib/module/components/ui/Floating-Action.js.map +1 -0
- package/lib/module/components/ui/Greeting-Card.js +392 -0
- package/lib/module/components/ui/Greeting-Card.js.map +1 -0
- package/lib/module/components/ui/Hover-Card.js +96 -0
- package/lib/module/components/ui/Hover-Card.js.map +1 -0
- package/lib/module/components/ui/Icon.js +73 -0
- package/lib/module/components/ui/Icon.js.map +1 -0
- package/lib/module/components/ui/Input.js +74 -0
- package/lib/module/components/ui/Input.js.map +1 -0
- package/lib/module/components/ui/Label.js +44 -0
- package/lib/module/components/ui/Label.js.map +1 -0
- package/lib/module/components/ui/Menubar.js +375 -0
- package/lib/module/components/ui/Menubar.js.map +1 -0
- package/lib/module/components/ui/Native-Only-Animated-View.js +41 -0
- package/lib/module/components/ui/Native-Only-Animated-View.js.map +1 -0
- package/lib/module/components/ui/NavBar.js +352 -0
- package/lib/module/components/ui/NavBar.js.map +1 -0
- package/lib/module/components/ui/Popover.js +101 -0
- package/lib/module/components/ui/Popover.js.map +1 -0
- package/lib/module/components/ui/Progress.js +124 -0
- package/lib/module/components/ui/Progress.js.map +1 -0
- package/lib/module/components/ui/Radio-Group.js +75 -0
- package/lib/module/components/ui/Radio-Group.js.map +1 -0
- package/lib/module/components/ui/Select.js +269 -0
- package/lib/module/components/ui/Select.js.map +1 -0
- package/lib/module/components/ui/Separator.js +58 -0
- package/lib/module/components/ui/Separator.js.map +1 -0
- package/lib/module/components/ui/SizedBox.js +101 -0
- package/lib/module/components/ui/SizedBox.js.map +1 -0
- package/lib/module/components/ui/Skeleton.js +57 -0
- package/lib/module/components/ui/Skeleton.js.map +1 -0
- package/lib/module/components/ui/Slider.js +169 -0
- package/lib/module/components/ui/Slider.js.map +1 -0
- package/lib/module/components/ui/Switch.js +55 -0
- package/lib/module/components/ui/Switch.js.map +1 -0
- package/lib/module/components/ui/Table.js +150 -0
- package/lib/module/components/ui/Table.js.map +1 -0
- package/lib/module/components/ui/Tabs.js +106 -0
- package/lib/module/components/ui/Tabs.js.map +1 -0
- package/lib/module/components/ui/Text.js +69 -0
- package/lib/module/components/ui/Text.js.map +1 -0
- package/lib/module/components/ui/Textarea.js +88 -0
- package/lib/module/components/ui/Textarea.js.map +1 -0
- package/lib/module/components/ui/Theme-Toggle.js +156 -0
- package/lib/module/components/ui/Theme-Toggle.js.map +1 -0
- package/lib/module/components/ui/Toast.js +101 -0
- package/lib/module/components/ui/Toast.js.map +1 -0
- package/lib/module/components/ui/Toggle-Group.js +129 -0
- package/lib/module/components/ui/Toggle-Group.js.map +1 -0
- package/lib/module/components/ui/Toggle.js +106 -0
- package/lib/module/components/ui/Toggle.js.map +1 -0
- package/lib/module/components/ui/Tooltip.js +106 -0
- package/lib/module/components/ui/Tooltip.js.map +1 -0
- package/lib/module/components/ui/index.js +45 -0
- package/lib/module/components/ui/index.js.map +1 -0
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/lib/ThemeProvider.js +173 -0
- package/lib/module/lib/ThemeProvider.js.map +1 -0
- package/lib/module/lib/cornerRadius.js +164 -0
- package/lib/module/lib/cornerRadius.js.map +1 -0
- package/lib/module/lib/fonts.js +25 -0
- package/lib/module/lib/fonts.js.map +1 -0
- package/lib/module/lib/theme.js +212 -0
- package/lib/module/lib/theme.js.map +1 -0
- package/lib/module/lib/utils.js +137 -0
- package/lib/module/lib/utils.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/package.json +208 -0
- package/src/app/_layout.tsx +25 -0
- package/src/assets/icons/weather_icons/drizzle.png +0 -0
- package/src/assets/icons/weather_icons/foggy.png +0 -0
- package/src/assets/icons/weather_icons/freezing_rain.png +0 -0
- package/src/assets/icons/weather_icons/partly_cloudy.png +0 -0
- package/src/assets/icons/weather_icons/rainy.png +0 -0
- package/src/assets/icons/weather_icons/showers.png +0 -0
- package/src/assets/icons/weather_icons/sunny_weather.png +0 -0
- package/src/assets/icons/weather_icons/thunderstorm.png +0 -0
- package/src/assets/icons/weather_icons/thunderstorm_hail.png +0 -0
- package/src/components/theme-config.ts +331 -0
- package/src/components/ui/Accordion.tsx +253 -0
- package/src/components/ui/Alert-Dialog.tsx +295 -0
- package/src/components/ui/Alert.tsx +137 -0
- package/src/components/ui/AppBar.tsx +551 -0
- package/src/components/ui/Aspect-Ratio.tsx +25 -0
- package/src/components/ui/Avatar.tsx +103 -0
- package/src/components/ui/Badge.tsx +121 -0
- package/src/components/ui/Bottom-Sheet.tsx +224 -0
- package/src/components/ui/Button.tsx +100 -0
- package/src/components/ui/Card.tsx +185 -0
- package/src/components/ui/Checkbox.tsx +81 -0
- package/src/components/ui/Collapsible.tsx +40 -0
- package/src/components/ui/Context-Menu.tsx +407 -0
- package/src/components/ui/Custom-Card.tsx +226 -0
- package/src/components/ui/Dialog.tsx +240 -0
- package/src/components/ui/Dropdown-Menu.tsx +544 -0
- package/src/components/ui/Floating-Action.tsx +54 -0
- package/src/components/ui/Greeting-Card.tsx +471 -0
- package/src/components/ui/Hover-Card.tsx +101 -0
- package/src/components/ui/Icon.tsx +75 -0
- package/src/components/ui/Input.tsx +90 -0
- package/src/components/ui/Label.tsx +48 -0
- package/src/components/ui/Menubar.tsx +509 -0
- package/src/components/ui/Native-Only-Animated-View.tsx +37 -0
- package/src/components/ui/NavBar.tsx +397 -0
- package/src/components/ui/Popover.tsx +110 -0
- package/src/components/ui/Progress.tsx +138 -0
- package/src/components/ui/Radio-Group.tsx +79 -0
- package/src/components/ui/Select.tsx +344 -0
- package/src/components/ui/Separator.tsx +68 -0
- package/src/components/ui/SizedBox.tsx +116 -0
- package/src/components/ui/Skeleton.tsx +55 -0
- package/src/components/ui/Slider.tsx +222 -0
- package/src/components/ui/Switch.tsx +67 -0
- package/src/components/ui/Table.tsx +170 -0
- package/src/components/ui/Tabs.tsx +119 -0
- package/src/components/ui/Text.tsx +73 -0
- package/src/components/ui/Textarea.tsx +93 -0
- package/src/components/ui/Theme-Toggle.tsx +204 -0
- package/src/components/ui/Toast.tsx +127 -0
- package/src/components/ui/Toggle-Group.tsx +160 -0
- package/src/components/ui/Toggle.tsx +122 -0
- package/src/components/ui/Tooltip.tsx +117 -0
- package/src/components/ui/index.ts +42 -0
- package/src/index.tsx +24 -0
- package/src/lib/ThemeProvider.tsx +204 -0
- package/src/lib/cornerRadius.ts +160 -0
- package/src/lib/fonts.ts +28 -0
- package/src/lib/theme.ts +151 -0
- package/src/lib/utils.ts +146 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Image,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
View,
|
|
7
|
+
useColorScheme,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { cssInterop } from 'nativewind';
|
|
10
|
+
import { cn } from '../../lib/utils';
|
|
11
|
+
import { CustomCard } from './Custom-Card';
|
|
12
|
+
import { Text } from './Text';
|
|
13
|
+
|
|
14
|
+
cssInterop(View, { className: 'style' });
|
|
15
|
+
cssInterop(Pressable, { className: 'style' });
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Weather data structure for the greeting card
|
|
19
|
+
*/
|
|
20
|
+
export interface Weather {
|
|
21
|
+
/** Temperature in Celsius */
|
|
22
|
+
temperature: number;
|
|
23
|
+
/** Human-readable weather description (e.g., "Clear Sky", "Rainy") */
|
|
24
|
+
weatherDescription: string;
|
|
25
|
+
/** Path to the weather icon image */
|
|
26
|
+
weatherImage: any;
|
|
27
|
+
/** Weather code from Open-Meteo API */
|
|
28
|
+
weatherCode: number;
|
|
29
|
+
/** Timestamp of last weather update */
|
|
30
|
+
lastUpdated: Date;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Weather icon mappings for different weather conditions
|
|
35
|
+
*/
|
|
36
|
+
const WEATHER_ICONS = {
|
|
37
|
+
sunny: require('../../assets/icons/weather_icons/sunny_weather.png'),
|
|
38
|
+
partlyCloudy: require('../../assets/icons/weather_icons/partly_cloudy.png'),
|
|
39
|
+
foggy: require('../../assets/icons/weather_icons/foggy.png'),
|
|
40
|
+
drizzle: require('../../assets/icons/weather_icons/drizzle.png'),
|
|
41
|
+
rainy: require('../../assets/icons/weather_icons/rainy.png'),
|
|
42
|
+
freezingRain: require('../../assets/icons/weather_icons/freezing_rain.png'),
|
|
43
|
+
showers: require('../../assets/icons/weather_icons/showers.png'),
|
|
44
|
+
thunderstorm: require('../../assets/icons/weather_icons/thunderstorm.png'),
|
|
45
|
+
thunderstormHail: require('../../assets/icons/weather_icons/thunderstorm_hail.png'),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Converts a weather code from Open-Meteo API to a description and icon
|
|
50
|
+
*
|
|
51
|
+
* @param {number} code - Weather code from Open-Meteo API (0-99)
|
|
52
|
+
* @returns {Object} Object containing description and icon path
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const weatherInfo = getWeatherDescription(0);
|
|
57
|
+
* // Returns: { description: 'Clear Sky (Sunny)', weatherImage: ... }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function getWeatherDescription(code: number): {
|
|
61
|
+
description: string;
|
|
62
|
+
weatherImage: any;
|
|
63
|
+
} {
|
|
64
|
+
const weatherMap: {
|
|
65
|
+
[key: string]: { description: string; icon: keyof typeof WEATHER_ICONS };
|
|
66
|
+
} = {
|
|
67
|
+
'0': { description: 'Clear Sky (Sunny)', icon: 'sunny' },
|
|
68
|
+
'1,2,3': { description: 'Partly Cloudy', icon: 'partlyCloudy' },
|
|
69
|
+
'45,48': { description: 'Foggy', icon: 'foggy' },
|
|
70
|
+
'51,53,55': { description: 'Drizzle', icon: 'drizzle' },
|
|
71
|
+
'56,57': { description: 'Freezing Drizzle', icon: 'drizzle' },
|
|
72
|
+
'61,63,65': { description: 'Rainy', icon: 'rainy' },
|
|
73
|
+
'66,67': { description: 'Freezing Rain', icon: 'freezingRain' },
|
|
74
|
+
'80,81,82': { description: 'Showers', icon: 'showers' },
|
|
75
|
+
'95': { description: 'Thunderstorm', icon: 'thunderstorm' },
|
|
76
|
+
'96,99': {
|
|
77
|
+
description: 'Thunderstorm with Hail',
|
|
78
|
+
icon: 'thunderstormHail',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (const [codes, info] of Object.entries(weatherMap)) {
|
|
83
|
+
if (codes.split(',').map(Number).includes(code)) {
|
|
84
|
+
return {
|
|
85
|
+
description: info.description,
|
|
86
|
+
weatherImage: WEATHER_ICONS[info.icon],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
description: 'Unknown Weather',
|
|
93
|
+
weatherImage: WEATHER_ICONS.sunny,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns a greeting based on the current time of day
|
|
99
|
+
*
|
|
100
|
+
* @returns {string} Time-appropriate greeting
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* const greeting = getTimeBasedGreeting();
|
|
105
|
+
* // Returns: "Good Morning" (if it's morning)
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function getTimeBasedGreeting(): string {
|
|
109
|
+
const hour = new Date().getHours();
|
|
110
|
+
|
|
111
|
+
if (hour >= 5 && hour < 12) {
|
|
112
|
+
return 'Good Morning';
|
|
113
|
+
} else if (hour >= 12 && hour < 17) {
|
|
114
|
+
return 'Good Afternoon';
|
|
115
|
+
} else if (hour >= 17 && hour < 21) {
|
|
116
|
+
return 'Good Evening';
|
|
117
|
+
} else {
|
|
118
|
+
return 'Good Night';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Formats a date to a readable string format
|
|
124
|
+
*
|
|
125
|
+
* @param {Date} date - Date to format
|
|
126
|
+
* @returns {string} Formatted date string (e.g., "Monday, 16 October")
|
|
127
|
+
*/
|
|
128
|
+
export function formatDate(date: Date): string {
|
|
129
|
+
const days = [
|
|
130
|
+
'Sunday',
|
|
131
|
+
'Monday',
|
|
132
|
+
'Tuesday',
|
|
133
|
+
'Wednesday',
|
|
134
|
+
'Thursday',
|
|
135
|
+
'Friday',
|
|
136
|
+
'Saturday',
|
|
137
|
+
];
|
|
138
|
+
const months = [
|
|
139
|
+
'January',
|
|
140
|
+
'February',
|
|
141
|
+
'March',
|
|
142
|
+
'April',
|
|
143
|
+
'May',
|
|
144
|
+
'June',
|
|
145
|
+
'July',
|
|
146
|
+
'August',
|
|
147
|
+
'September',
|
|
148
|
+
'October',
|
|
149
|
+
'November',
|
|
150
|
+
'December',
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const dayName = days[date.getDay()];
|
|
154
|
+
const day = date.getDate();
|
|
155
|
+
const monthName = months[date.getMonth()];
|
|
156
|
+
|
|
157
|
+
return `${dayName}, ${day} ${monthName}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Fetches weather data from Open-Meteo API
|
|
162
|
+
*
|
|
163
|
+
* @param {number} latitude - Latitude coordinate
|
|
164
|
+
* @param {number} longitude - Longitude coordinate
|
|
165
|
+
* @returns {Promise<Weather | null>} Weather data or null if fetch fails
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```tsx
|
|
169
|
+
* const weather = await fetchWeather(37.7749, -122.4194);
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
export async function fetchWeather(
|
|
173
|
+
latitude: number,
|
|
174
|
+
longitude: number
|
|
175
|
+
): Promise<Weather | null> {
|
|
176
|
+
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,weathercode`;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch(url);
|
|
180
|
+
|
|
181
|
+
if (response.ok) {
|
|
182
|
+
const data = await response.json();
|
|
183
|
+
|
|
184
|
+
const temperature: number = data.current?.temperature_2m ?? 0;
|
|
185
|
+
const weatherCode: number = data.current?.weathercode ?? 0;
|
|
186
|
+
|
|
187
|
+
const weatherInfo = getWeatherDescription(weatherCode);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
temperature,
|
|
191
|
+
weatherDescription: weatherInfo.description,
|
|
192
|
+
weatherImage: weatherInfo.weatherImage,
|
|
193
|
+
weatherCode,
|
|
194
|
+
lastUpdated: new Date(),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return null;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('Failed to fetch weather:', error);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Props for the GreetingCard component
|
|
207
|
+
*/
|
|
208
|
+
export interface GreetingCardProps {
|
|
209
|
+
/** User's full name to display in the greeting (required) */
|
|
210
|
+
userName: string;
|
|
211
|
+
/** Latitude coordinate for weather fetching (optional - weather shown only when both lat and lng provided) */
|
|
212
|
+
lat?: number;
|
|
213
|
+
/** Longitude coordinate for weather fetching (optional - weather shown only when both lat and lng provided) */
|
|
214
|
+
lng?: number;
|
|
215
|
+
/** Custom greeting message (overrides time-based greeting) */
|
|
216
|
+
customGreeting?: string;
|
|
217
|
+
/** Custom date to display (defaults to current date) */
|
|
218
|
+
date?: Date;
|
|
219
|
+
/** Action button text (e.g., "View Schedule") */
|
|
220
|
+
actionText?: string;
|
|
221
|
+
/** Callback when the card or action button is pressed */
|
|
222
|
+
onPress?: () => void;
|
|
223
|
+
/** Additional CSS classes for the card container */
|
|
224
|
+
className?: string;
|
|
225
|
+
/** Whether to show the action button section */
|
|
226
|
+
showAction?: boolean;
|
|
227
|
+
/** Cache duration in milliseconds (default: 15 minutes) */
|
|
228
|
+
cacheDuration?: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* GreetingCard - A personalized greeting card with weather information
|
|
233
|
+
*
|
|
234
|
+
* Displays a personalized greeting with the user's name, current date, and optional
|
|
235
|
+
* weather information fetched from Open-Meteo API. Includes an optional action button
|
|
236
|
+
* (e.g., "View Schedule"). Weather data is automatically cached for 15 minutes.
|
|
237
|
+
*
|
|
238
|
+
* @component
|
|
239
|
+
* @example
|
|
240
|
+
* ```tsx
|
|
241
|
+
* // Basic usage without weather
|
|
242
|
+
* <GreetingCard
|
|
243
|
+
* userName="John Doe"
|
|
244
|
+
* actionText="View Schedule"
|
|
245
|
+
* onPress={() => navigation.navigate('Schedule')}
|
|
246
|
+
* />
|
|
247
|
+
*
|
|
248
|
+
* // With automatic weather fetching (provide both lat and lng)
|
|
249
|
+
* <GreetingCard
|
|
250
|
+
* userName="Jane Smith"
|
|
251
|
+
* lat={37.7749}
|
|
252
|
+
* lng={-122.4194}
|
|
253
|
+
* actionText="View Dashboard"
|
|
254
|
+
* onPress={() => console.log('Card pressed')}
|
|
255
|
+
* />
|
|
256
|
+
*
|
|
257
|
+
* // Custom greeting without action
|
|
258
|
+
* <GreetingCard
|
|
259
|
+
* userName="Alice"
|
|
260
|
+
* customGreeting="Welcome back"
|
|
261
|
+
* showAction={false}
|
|
262
|
+
* />
|
|
263
|
+
* ```
|
|
264
|
+
*
|
|
265
|
+
* @property {string} userName - User's full name to display in greeting (required)
|
|
266
|
+
* @property {number} [lat] - Latitude coordinate for weather fetching (optional)
|
|
267
|
+
* @property {number} [lng] - Longitude coordinate for weather fetching (optional)
|
|
268
|
+
* @property {string} [customGreeting] - Override the time-based greeting
|
|
269
|
+
* @property {Date} [date] - Custom date to display (defaults to current date)
|
|
270
|
+
* @property {string} [actionText="View Details"] - Text for the action button
|
|
271
|
+
* @property {Function} [onPress] - Handler called when card or action is pressed
|
|
272
|
+
* @property {string} [className] - Additional Tailwind classes for styling
|
|
273
|
+
* @property {boolean} [showAction=true] - Whether to show the action button section
|
|
274
|
+
* @property {number} [cacheDuration=900000] - Cache duration in ms (default: 15 min)
|
|
275
|
+
*/
|
|
276
|
+
export function GreetingCard({
|
|
277
|
+
userName,
|
|
278
|
+
lat,
|
|
279
|
+
lng,
|
|
280
|
+
customGreeting,
|
|
281
|
+
date = new Date(),
|
|
282
|
+
actionText = 'View Details',
|
|
283
|
+
onPress,
|
|
284
|
+
className,
|
|
285
|
+
showAction = true,
|
|
286
|
+
cacheDuration = 15 * 60 * 1000, // 15 minutes
|
|
287
|
+
}: GreetingCardProps) {
|
|
288
|
+
const colorScheme = useColorScheme();
|
|
289
|
+
const isDark = colorScheme === 'dark';
|
|
290
|
+
const [weather, setWeather] = React.useState<Weather | null>(null);
|
|
291
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
292
|
+
const cacheRef = React.useRef<{
|
|
293
|
+
data: Weather | null;
|
|
294
|
+
timestamp: number;
|
|
295
|
+
} | null>(null);
|
|
296
|
+
|
|
297
|
+
// Dynamic styles based on theme
|
|
298
|
+
const dynamicStyles = React.useMemo(
|
|
299
|
+
() => ({
|
|
300
|
+
separator: {
|
|
301
|
+
...styles.separator,
|
|
302
|
+
backgroundColor: isDark
|
|
303
|
+
? 'rgba(255, 255, 255, 0.15)'
|
|
304
|
+
: 'rgba(0, 0, 0, 0.1)',
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
[isDark]
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
React.useEffect(() => {
|
|
311
|
+
const loadWeather = async () => {
|
|
312
|
+
if (lat === undefined || lng === undefined) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
|
|
318
|
+
// Check cache
|
|
319
|
+
if (
|
|
320
|
+
cacheRef.current &&
|
|
321
|
+
now - cacheRef.current.timestamp < cacheDuration
|
|
322
|
+
) {
|
|
323
|
+
setWeather(cacheRef.current.data);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
setIsLoading(true);
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const weatherData = await fetchWeather(lat, lng);
|
|
331
|
+
setWeather(weatherData);
|
|
332
|
+
cacheRef.current = {
|
|
333
|
+
data: weatherData,
|
|
334
|
+
timestamp: now,
|
|
335
|
+
};
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.error('Error loading weather:', error);
|
|
338
|
+
} finally {
|
|
339
|
+
setIsLoading(false);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
loadWeather();
|
|
344
|
+
}, [lat, lng, cacheDuration]);
|
|
345
|
+
|
|
346
|
+
const greeting = customGreeting || getTimeBasedGreeting();
|
|
347
|
+
const formattedDate = formatDate(date);
|
|
348
|
+
|
|
349
|
+
const CardWrapper = onPress ? Pressable : View;
|
|
350
|
+
const wrapperProps = onPress
|
|
351
|
+
? {
|
|
352
|
+
onPress,
|
|
353
|
+
android_ripple: { color: 'rgba(0, 0, 0, 0.1)' },
|
|
354
|
+
}
|
|
355
|
+
: {};
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<CardWrapper {...wrapperProps}>
|
|
359
|
+
<CustomCard
|
|
360
|
+
cornerRadius={20}
|
|
361
|
+
cornerSmoothing={1}
|
|
362
|
+
className={cn('border-primary/20 dark:border-primary/30', className)}
|
|
363
|
+
>
|
|
364
|
+
{/* Wrapper with negative margin to counteract CustomCard's default padding */}
|
|
365
|
+
<View style={styles.contentWrapper}>
|
|
366
|
+
{/* Header Section with Greeting and Weather */}
|
|
367
|
+
<View style={styles.headerSection}>
|
|
368
|
+
{/* Greeting Text */}
|
|
369
|
+
<View style={styles.greetingText}>
|
|
370
|
+
<Text className="text-sm font-semibold text-card-foreground dark:text-card-foreground">
|
|
371
|
+
Hi {userName},
|
|
372
|
+
</Text>
|
|
373
|
+
<Text className="mt-1.5 text-xs tracking-wider text-muted-foreground dark:text-muted-foreground">
|
|
374
|
+
{greeting}, {formattedDate}
|
|
375
|
+
</Text>
|
|
376
|
+
</View>
|
|
377
|
+
|
|
378
|
+
{/* Weather Display */}
|
|
379
|
+
{(weather || isLoading) && (
|
|
380
|
+
<View style={styles.weatherContainer}>
|
|
381
|
+
{isLoading ? (
|
|
382
|
+
<Text className="text-xs text-muted-foreground dark:text-muted-foreground">
|
|
383
|
+
Loading...
|
|
384
|
+
</Text>
|
|
385
|
+
) : (
|
|
386
|
+
weather && (
|
|
387
|
+
<>
|
|
388
|
+
<View style={styles.weatherRow}>
|
|
389
|
+
<Image
|
|
390
|
+
source={weather.weatherImage}
|
|
391
|
+
style={styles.weatherIcon}
|
|
392
|
+
resizeMode="contain"
|
|
393
|
+
/>
|
|
394
|
+
<Text className="ml-1 text-xs text-card-foreground dark:text-card-foreground">
|
|
395
|
+
{Math.round(weather.temperature)}° C
|
|
396
|
+
</Text>
|
|
397
|
+
</View>
|
|
398
|
+
<Text className="mt-0.5 text-xs text-muted-foreground/70 dark:text-muted-foreground/70">
|
|
399
|
+
{weather.weatherDescription}
|
|
400
|
+
</Text>
|
|
401
|
+
</>
|
|
402
|
+
)
|
|
403
|
+
)}
|
|
404
|
+
</View>
|
|
405
|
+
)}
|
|
406
|
+
</View>
|
|
407
|
+
|
|
408
|
+
{/* Action Section */}
|
|
409
|
+
{showAction && (
|
|
410
|
+
<>
|
|
411
|
+
<View style={dynamicStyles.separator} />
|
|
412
|
+
<View style={styles.actionSection}>
|
|
413
|
+
<Text className="text-sm font-semibold text-card-foreground dark:text-card-foreground">
|
|
414
|
+
{actionText}
|
|
415
|
+
</Text>
|
|
416
|
+
<Text className="text-2xl text-primary dark:text-primary">
|
|
417
|
+
›
|
|
418
|
+
</Text>
|
|
419
|
+
</View>
|
|
420
|
+
</>
|
|
421
|
+
)}
|
|
422
|
+
</View>
|
|
423
|
+
</CustomCard>
|
|
424
|
+
</CardWrapper>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
GreetingCard.displayName = 'GreetingCard';
|
|
429
|
+
|
|
430
|
+
const styles = StyleSheet.create({
|
|
431
|
+
weatherIcon: {
|
|
432
|
+
width: 40,
|
|
433
|
+
height: 35,
|
|
434
|
+
},
|
|
435
|
+
contentWrapper: {
|
|
436
|
+
margin: -24, // Counteract CustomCard's default padding of 24
|
|
437
|
+
},
|
|
438
|
+
headerSection: {
|
|
439
|
+
flexDirection: 'row',
|
|
440
|
+
alignItems: 'flex-start',
|
|
441
|
+
justifyContent: 'space-between',
|
|
442
|
+
paddingHorizontal: 20,
|
|
443
|
+
paddingTop: 20,
|
|
444
|
+
paddingBottom: 16,
|
|
445
|
+
},
|
|
446
|
+
greetingText: {
|
|
447
|
+
flex: 1,
|
|
448
|
+
},
|
|
449
|
+
weatherContainer: {
|
|
450
|
+
marginLeft: 12,
|
|
451
|
+
alignItems: 'flex-end',
|
|
452
|
+
},
|
|
453
|
+
weatherRow: {
|
|
454
|
+
flexDirection: 'row',
|
|
455
|
+
alignItems: 'center',
|
|
456
|
+
},
|
|
457
|
+
separator: {
|
|
458
|
+
height: 1,
|
|
459
|
+
marginVertical: 8,
|
|
460
|
+
},
|
|
461
|
+
actionSection: {
|
|
462
|
+
flexDirection: 'row',
|
|
463
|
+
alignItems: 'center',
|
|
464
|
+
justifyContent: 'space-between',
|
|
465
|
+
paddingHorizontal: 20,
|
|
466
|
+
paddingBottom: 12,
|
|
467
|
+
paddingTop: 4,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
export { WEATHER_ICONS };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NativeOnlyAnimatedView } from './Native-Only-Animated-View';
|
|
2
|
+
import { TextClassContext } from './Text';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import * as HoverCardPrimitive from '@rn-primitives/hover-card';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { Platform, StyleSheet } from 'react-native';
|
|
7
|
+
import { FadeIn, FadeOut } from 'react-native-reanimated';
|
|
8
|
+
import { FullWindowOverlay as RNFullWindowOverlay } from 'react-native-screens';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Root component for hover card - provides context for trigger and content
|
|
12
|
+
*
|
|
13
|
+
* @component
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <HoverCard>
|
|
17
|
+
* <HoverCardTrigger>
|
|
18
|
+
* <Text>Hover over me</Text>
|
|
19
|
+
* </HoverCardTrigger>
|
|
20
|
+
* <HoverCardContent>
|
|
21
|
+
* <Text>Additional information appears here</Text>
|
|
22
|
+
* </HoverCardContent>
|
|
23
|
+
* </HoverCard>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
const HoverCard = HoverCardPrimitive.Root;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Trigger component that shows hover card content on hover/press
|
|
30
|
+
*
|
|
31
|
+
* @component
|
|
32
|
+
*/
|
|
33
|
+
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Full window overlay wrapper for iOS, Fragment for other platforms
|
|
37
|
+
*/
|
|
38
|
+
const FullWindowOverlay =
|
|
39
|
+
Platform.OS === 'ios' ? RNFullWindowOverlay : React.Fragment;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Content container for hover card
|
|
43
|
+
*
|
|
44
|
+
* Displays rich content in a popover when hovering over or pressing the trigger.
|
|
45
|
+
* Includes smooth fade animations and proper positioning.
|
|
46
|
+
*
|
|
47
|
+
* @component
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* <HoverCardContent align="center" sideOffset={8}>
|
|
51
|
+
* <View className="gap-2">
|
|
52
|
+
* <Text className="font-semibold">User Profile</Text>
|
|
53
|
+
* <Text className="text-sm">Additional details about the user</Text>
|
|
54
|
+
* </View>
|
|
55
|
+
* </HoverCardContent>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @accessibility
|
|
59
|
+
* - Content is announced by screen readers
|
|
60
|
+
* - Proper focus management on web
|
|
61
|
+
*/
|
|
62
|
+
function HoverCardContent({
|
|
63
|
+
className,
|
|
64
|
+
align = 'center',
|
|
65
|
+
sideOffset = 4,
|
|
66
|
+
...props
|
|
67
|
+
}: HoverCardPrimitive.ContentProps &
|
|
68
|
+
React.RefAttributes<HoverCardPrimitive.ContentRef>) {
|
|
69
|
+
return (
|
|
70
|
+
<HoverCardPrimitive.Portal>
|
|
71
|
+
<FullWindowOverlay>
|
|
72
|
+
<HoverCardPrimitive.Overlay
|
|
73
|
+
style={Platform.select({ native: StyleSheet.absoluteFill })}
|
|
74
|
+
>
|
|
75
|
+
<NativeOnlyAnimatedView entering={FadeIn} exiting={FadeOut}>
|
|
76
|
+
<TextClassContext.Provider value="text-popover-foreground">
|
|
77
|
+
<HoverCardPrimitive.Content
|
|
78
|
+
align={align}
|
|
79
|
+
sideOffset={sideOffset}
|
|
80
|
+
className={cn(
|
|
81
|
+
'bg-popover border-border outline-hidden z-50 w-64 rounded-md border p-4 shadow-md shadow-black/5',
|
|
82
|
+
Platform.select({
|
|
83
|
+
web: cn(
|
|
84
|
+
'animate-in fade-in-0 zoom-in-95 origin-(--radix-hover-card-content-transform-origin) cursor-default [&>*]:cursor-auto',
|
|
85
|
+
props.side === 'bottom' && 'slide-in-from-top-2',
|
|
86
|
+
props.side === 'top' && 'slide-in-from-bottom-2'
|
|
87
|
+
),
|
|
88
|
+
}),
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
</TextClassContext.Provider>
|
|
94
|
+
</NativeOnlyAnimatedView>
|
|
95
|
+
</HoverCardPrimitive.Overlay>
|
|
96
|
+
</FullWindowOverlay>
|
|
97
|
+
</HoverCardPrimitive.Portal>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { HoverCard, HoverCardContent, HoverCardTrigger };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils';
|
|
2
|
+
import type { LucideIcon, LucideProps } from 'lucide-react-native';
|
|
3
|
+
import { cssInterop } from 'nativewind';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Props for Icon component
|
|
7
|
+
*
|
|
8
|
+
* @extends LucideProps - All Lucide icon properties (size, color, strokeWidth, etc.)
|
|
9
|
+
* @property {LucideIcon} as - The Lucide icon component to render
|
|
10
|
+
* @property {string} [className] - Tailwind classes for styling (supports NativeWind)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { Home } from 'lucide-react-native';
|
|
15
|
+
* <Icon as={Home} size={24} className="text-primary" />
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
type IconProps = LucideProps & {
|
|
19
|
+
as: LucideIcon;
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function IconImpl({ as: IconComponent, ...props }: IconProps) {
|
|
24
|
+
return <IconComponent {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
cssInterop(IconImpl, {
|
|
28
|
+
className: {
|
|
29
|
+
target: 'style',
|
|
30
|
+
nativeStyleToProp: {
|
|
31
|
+
height: 'size',
|
|
32
|
+
width: 'size',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Universal icon wrapper for Lucide icons
|
|
39
|
+
*
|
|
40
|
+
* Provides consistent styling and sizing for Lucide React Native icons across the application.
|
|
41
|
+
* Automatically handles platform-specific sizing and color theming.
|
|
42
|
+
*
|
|
43
|
+
* @component
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* import { Settings, User, ChevronRight } from 'lucide-react-native';
|
|
47
|
+
*
|
|
48
|
+
* // Basic icon
|
|
49
|
+
* <Icon as={Settings} />
|
|
50
|
+
*
|
|
51
|
+
* // Sized icon with custom color
|
|
52
|
+
* <Icon as={User} size={32} className="text-primary" />
|
|
53
|
+
*
|
|
54
|
+
* // Icon with stroke width
|
|
55
|
+
* <Icon as={ChevronRight} size={20} strokeWidth={2.5} />
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
function Icon({
|
|
59
|
+
as: IconComponent,
|
|
60
|
+
className,
|
|
61
|
+
size = 14,
|
|
62
|
+
...props
|
|
63
|
+
}: IconProps) {
|
|
64
|
+
return (
|
|
65
|
+
<IconImpl
|
|
66
|
+
as={IconComponent}
|
|
67
|
+
className={cn('text-foreground', className)}
|
|
68
|
+
size={size}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { Icon };
|
|
75
|
+
export type { IconProps };
|