Skip to content

Commit e1a6da4

Browse files
committed
Improve timezone handling and update Discord links
Enhanced schedule calculation logic to handle timezones accurately in ScheduleListModal and schedule-rebalance/utils. Updated all Discord invite links to use the new mee6.xyz format across the app. Added Widgetbot Discord widget to index.html, improved Pricing page icons and copy, and made minor UI/wording tweaks.
1 parent 4e877a7 commit e1a6da4

File tree

11 files changed

+359
-190
lines changed

11 files changed

+359
-190
lines changed

index.html

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
content="AI-powered trading system with 11 specialized agents for comprehensive market analysis" />
1111
<meta name="author" content="TradingGoose" />
1212

13-
<meta property="og:title" content="TradingGoose AI" />
13+
<meta property="og:title" content="TradingGoose" />
1414
<meta property="og:description"
15-
content="AI-powered trading system with 11 specialized agents for comprehensive market analysis" />
15+
content="AI-powered LLM portfolio management with 15 specialized agents for comprehensive market analysis" />
1616
<meta property="og:type" content="website" />
1717
<meta property="og:image" content="/Social-Preview.png" />
1818

@@ -97,6 +97,42 @@
9797
<body>
9898
<div id="root"></div>
9999
<script type="module" src="/src/main.tsx"></script>
100+
101+
<!-- Widgetbot Crate for Discord -->
102+
<script src="https://cdn.jsdelivr.net/npm/@widgetbot/crate@3" async defer></script>
103+
<script>
104+
// Wait for page to fully load before initializing Widgetbot
105+
window.addEventListener('load', function () {
106+
// 3 second delay after page load with fade-in animation
107+
setTimeout(function () {
108+
new Crate({
109+
server: '1410785898273964166', // Replace with your Discord server ID
110+
channel: '1411021392983363616', // Replace with your Discord channel ID
111+
location: ['bottom', 'right'], // Position at bottom right
112+
color: '#ffcc00', // Discord's default color, you can change this
113+
notifications: true, // Show notification badge
114+
indicator: true, // Show unread message indicator
115+
defer: true,
116+
css: `
117+
@keyframes fadeInScale {
118+
0% {
119+
opacity: 0;
120+
transform: scale(0.8);
121+
}
122+
100% {
123+
opacity: 1;
124+
transform: scale(1);
125+
}
126+
}
127+
128+
&[open="false"] {
129+
animation: fadeInScale 0.5s ease-out forwards;
130+
}
131+
`
132+
});
133+
}, 2000); // 2 seconds delay after page load
134+
});
135+
</script>
100136
</body>
101137

102138
</html>

src/components/Footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const Footer = () => {
5555
{/* Right Section - Social Media Icons */}
5656
<div className="flex items-center justify-center md:justify-end space-x-4">
5757
<a
58-
href="https://discord.gg/3dkTaNyp"
58+
href="https://mee6.xyz/i/mcEBEcvpOu"
5959
target="_blank"
6060
rel="noopener noreferrer"
6161
className="text-muted-foreground hover:text-primary transition-colors"

src/components/ScheduleListModal.tsx

Lines changed: 98 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -278,26 +278,93 @@ export default function ScheduleListModal({ isOpen, onClose }: ScheduleListModal
278278
const now = new Date();
279279
const [hours, minutes] = schedule.time_of_day.split(':').map(Number);
280280

281-
// Helper function to find next occurrence for weekly schedules with specific days
282-
const findNextWeeklyOccurrence = (startDate: Date, daysOfWeek: number[]): Date => {
283-
const result = new Date(startDate);
284-
result.setHours(hours, minutes, 0, 0);
281+
// Helper function to convert schedule time to UTC-aware date
282+
const createScheduledDate = (localDate: Date): Date => {
283+
// Format date for the target timezone
284+
const year = localDate.getFullYear();
285+
const month = String(localDate.getMonth() + 1).padStart(2, '0');
286+
const day = String(localDate.getDate()).padStart(2, '0');
287+
const hourStr = String(hours).padStart(2, '0');
288+
const minuteStr = String(minutes).padStart(2, '0');
289+
290+
// Create an ISO string for the target timezone time
291+
const dateTimeStr = `${year}-${month}-${day}T${hourStr}:${minuteStr}:00`;
292+
293+
// Use Intl.DateTimeFormat to handle timezone conversion properly
294+
// This simulates the SQL AT TIME ZONE behavior
295+
const formatter = new Intl.DateTimeFormat('en-US', {
296+
timeZone: schedule.timezone,
297+
year: 'numeric',
298+
month: '2-digit',
299+
day: '2-digit',
300+
hour: '2-digit',
301+
minute: '2-digit',
302+
second: '2-digit',
303+
hour12: false
304+
});
305+
306+
// Parse the target date/time in the schedule's timezone
307+
const testDate = new Date(dateTimeStr);
308+
const parts = formatter.formatToParts(testDate);
309+
const dateParts: any = {};
310+
parts.forEach(part => dateParts[part.type] = part.value);
285311

286-
// If the time has already passed today, start checking from tomorrow
287-
if (result <= now) {
288-
result.setDate(result.getDate() + 1);
312+
// Get timezone offset for this specific date/time
313+
const tzFormatter = new Intl.DateTimeFormat('en-US', {
314+
timeZone: schedule.timezone,
315+
timeZoneName: 'longOffset'
316+
});
317+
const tzParts = tzFormatter.formatToParts(new Date(dateTimeStr));
318+
const offsetStr = tzParts.find(p => p.type === 'timeZoneName')?.value || 'GMT+00:00';
319+
const match = offsetStr.match(/GMT([+-]\d{2}):(\d{2})/);
320+
let offsetMinutes = 0;
321+
if (match) {
322+
const offsetHours = parseInt(match[1]);
323+
const offsetMins = parseInt(match[2]);
324+
offsetMinutes = offsetHours * 60 + (offsetHours < 0 ? -offsetMins : offsetMins);
289325
}
290326

291-
// Find the next matching day of week (max 7 days to check)
327+
// Create the date in the local timezone and adjust for the schedule's timezone offset
328+
const localTime = new Date(dateTimeStr);
329+
const utcTime = localTime.getTime() - (offsetMinutes * 60 * 1000);
330+
return new Date(utcTime);
331+
};
332+
333+
// Helper function to find next occurrence for weekly schedules with specific days
334+
const findNextWeeklyOccurrence = (startDate: Date, daysOfWeek: number[]): Date => {
335+
let checkDate = new Date(startDate);
336+
337+
// Check up to 7 days ahead
292338
for (let i = 0; i < 7; i++) {
293-
if (daysOfWeek.includes(result.getDay())) {
294-
return result;
339+
// Get day of week in the schedule's timezone
340+
const formatter = new Intl.DateTimeFormat('en-US', {
341+
timeZone: schedule.timezone,
342+
weekday: 'long'
343+
});
344+
const dayName = formatter.format(checkDate);
345+
const dayMap: { [key: string]: number } = {
346+
'Sunday': 0,
347+
'Monday': 1,
348+
'Tuesday': 2,
349+
'Wednesday': 3,
350+
'Thursday': 4,
351+
'Friday': 5,
352+
'Saturday': 6
353+
};
354+
const currentDay = dayMap[dayName];
355+
356+
if (daysOfWeek.includes(currentDay)) {
357+
const scheduledTime = createScheduledDate(checkDate);
358+
if (scheduledTime > now) {
359+
return scheduledTime;
360+
}
295361
}
296-
result.setDate(result.getDate() + 1);
362+
363+
checkDate.setDate(checkDate.getDate() + 1);
297364
}
298365

299-
// Fallback (should never reach here if daysOfWeek is valid)
300-
return result;
366+
// Fallback
367+
return createScheduledDate(checkDate);
301368
};
302369

303370
// If never executed, calculate from current date
@@ -307,69 +374,70 @@ export default function ScheduleListModal({ isOpen, onClose }: ScheduleListModal
307374
return findNextWeeklyOccurrence(now, schedule.day_of_week);
308375
}
309376

310-
// Create a date in the schedule's timezone
311-
const nextRun = new Date(now);
312-
nextRun.setHours(hours, minutes, 0, 0);
377+
// Create a date at the scheduled time in the target timezone
378+
let nextRun = createScheduledDate(now);
313379

314380
// If that time has already passed today, add the interval
315381
if (nextRun <= now) {
382+
const nextDate = new Date(now);
316383
switch (schedule.interval_unit) {
317384
case 'days':
318-
nextRun.setDate(nextRun.getDate() + schedule.interval_value);
385+
nextDate.setDate(nextDate.getDate() + schedule.interval_value);
319386
break;
320387
case 'weeks':
321-
nextRun.setDate(nextRun.getDate() + (schedule.interval_value * 7));
388+
nextDate.setDate(nextDate.getDate() + (schedule.interval_value * 7));
322389
break;
323390
case 'months':
324-
nextRun.setMonth(nextRun.getMonth() + schedule.interval_value);
391+
nextDate.setMonth(nextDate.getMonth() + schedule.interval_value);
325392
break;
326393
}
394+
nextRun = createScheduledDate(nextDate);
327395
}
328396

329397
return nextRun;
330398
}
331399

332400
// Calculate from last execution
333401
const lastRun = new Date(schedule.last_executed_at);
334-
let nextRun = new Date(lastRun);
402+
let nextDate = new Date(lastRun);
335403

336404
// For weekly schedules with specific days
337405
if (schedule.interval_unit === 'weeks' && schedule.day_of_week && schedule.day_of_week.length > 0) {
338406
// Start from the day after last execution
339-
nextRun.setDate(nextRun.getDate() + 1);
340-
return findNextWeeklyOccurrence(nextRun, schedule.day_of_week);
407+
nextDate.setDate(nextDate.getDate() + 1);
408+
return findNextWeeklyOccurrence(nextDate, schedule.day_of_week);
341409
}
342410

343411
// Add the interval for regular schedules
344412
switch (schedule.interval_unit) {
345413
case 'days':
346-
nextRun.setDate(nextRun.getDate() + schedule.interval_value);
414+
nextDate.setDate(nextDate.getDate() + schedule.interval_value);
347415
break;
348416
case 'weeks':
349-
nextRun.setDate(nextRun.getDate() + (schedule.interval_value * 7));
417+
nextDate.setDate(nextDate.getDate() + (schedule.interval_value * 7));
350418
break;
351419
case 'months':
352-
nextRun.setMonth(nextRun.getMonth() + schedule.interval_value);
420+
nextDate.setMonth(nextDate.getMonth() + schedule.interval_value);
353421
break;
354422
}
355423

356-
// Set the proper time
357-
nextRun.setHours(hours, minutes, 0, 0);
424+
let nextRun = createScheduledDate(nextDate);
358425

359426
// IMPORTANT: If the calculated next run is in the past (e.g., schedule was paused),
360427
// advance it to the next valid future time
361428
while (nextRun <= now) {
362429
switch (schedule.interval_unit) {
363430
case 'days':
364-
nextRun.setDate(nextRun.getDate() + schedule.interval_value);
431+
nextDate.setDate(nextDate.getDate() + schedule.interval_value);
365432
break;
366433
case 'weeks':
367-
nextRun.setDate(nextRun.getDate() + (schedule.interval_value * 7));
434+
nextDate.setDate(nextDate.getDate() + (schedule.interval_value * 7));
368435
break;
369436
case 'months':
370-
nextRun.setMonth(nextRun.getMonth() + schedule.interval_value);
437+
nextDate.setMonth(nextDate.getMonth() + schedule.interval_value);
371438
break;
372439
}
440+
nextRun = createScheduledDate(nextDate);
373441
}
374442

375443
return nextRun;

src/components/schedule-rebalance/utils.ts

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,37 +36,97 @@ export const getNextRunTime = (config: ScheduleConfig): string => {
3636
hours = 0;
3737
}
3838

39-
let nextRun = new Date();
40-
nextRun.setHours(hours, minutes, 0, 0);
39+
// Helper function to create a date at the scheduled time in the target timezone
40+
const createScheduledDate = (localDate: Date): Date => {
41+
const year = localDate.getFullYear();
42+
const month = String(localDate.getMonth() + 1).padStart(2, '0');
43+
const day = String(localDate.getDate()).padStart(2, '0');
44+
const hourStr = String(hours).padStart(2, '0');
45+
const minuteStr = String(minutes).padStart(2, '0');
46+
47+
// Create an ISO string for the target timezone time
48+
const dateTimeStr = `${year}-${month}-${day}T${hourStr}:${minuteStr}:00`;
49+
50+
// Get timezone offset for this specific date/time
51+
const tzFormatter = new Intl.DateTimeFormat('en-US', {
52+
timeZone: config.timezone,
53+
timeZoneName: 'longOffset'
54+
});
55+
const tzParts = tzFormatter.formatToParts(new Date(dateTimeStr));
56+
const offsetStr = tzParts.find(p => p.type === 'timeZoneName')?.value || 'GMT+00:00';
57+
const match = offsetStr.match(/GMT([+-]\d{2}):(\d{2})/);
58+
let offsetMinutes = 0;
59+
if (match) {
60+
const offsetHours = parseInt(match[1]);
61+
const offsetMins = parseInt(match[2]);
62+
offsetMinutes = offsetHours * 60 + (offsetHours < 0 ? -offsetMins : offsetMins);
63+
}
64+
65+
// Create the date in the local timezone and adjust for the schedule's timezone offset
66+
const localTime = new Date(dateTimeStr);
67+
const utcTime = localTime.getTime() - (offsetMinutes * 60 * 1000);
68+
return new Date(utcTime);
69+
};
70+
71+
let nextDate = new Date(now);
72+
let nextRun = createScheduledDate(nextDate);
4173

4274
// Calculate based on interval unit
4375
if (config.intervalUnit === 'days') {
4476
// Add days until we find the next run time
4577
while (nextRun <= now) {
46-
nextRun.setDate(nextRun.getDate() + config.intervalValue);
78+
nextDate.setDate(nextDate.getDate() + config.intervalValue);
79+
nextRun = createScheduledDate(nextDate);
4780
}
4881
} else if (config.intervalUnit === 'weeks') {
49-
// Find next matching day of week
50-
while (nextRun <= now || !config.daysOfWeek.includes(nextRun.getDay())) {
51-
nextRun.setDate(nextRun.getDate() + 1);
82+
// Find next matching day of week in the schedule's timezone
83+
let found = false;
84+
for (let i = 0; i < 14; i++) { // Check up to 2 weeks ahead
85+
// Get day of week in the schedule's timezone
86+
const formatter = new Intl.DateTimeFormat('en-US', {
87+
timeZone: config.timezone,
88+
weekday: 'long'
89+
});
90+
const dayName = formatter.format(nextDate);
91+
const dayMap: { [key: string]: number } = {
92+
'Sunday': 0,
93+
'Monday': 1,
94+
'Tuesday': 2,
95+
'Wednesday': 3,
96+
'Thursday': 4,
97+
'Friday': 5,
98+
'Saturday': 6
99+
};
100+
const currentDay = dayMap[dayName];
101+
102+
if (config.daysOfWeek.includes(currentDay)) {
103+
nextRun = createScheduledDate(nextDate);
104+
if (nextRun > now) {
105+
found = true;
106+
break;
107+
}
108+
}
109+
nextDate.setDate(nextDate.getDate() + 1);
52110
}
53-
// If still in the past, add weeks
54-
if (nextRun <= now) {
55-
nextRun.setDate(nextRun.getDate() + (config.intervalValue * 7));
111+
112+
if (!found) {
113+
// Fallback to next occurrence
114+
nextRun = createScheduledDate(nextDate);
56115
}
57116
} else if (config.intervalUnit === 'months') {
58117
// Find next matching day of month
59-
while (nextRun <= now || !config.daysOfMonth.includes(nextRun.getDate())) {
60-
nextRun.setDate(nextRun.getDate() + 1);
118+
while (nextRun <= now || !config.daysOfMonth.includes(nextDate.getDate())) {
119+
nextDate.setDate(nextDate.getDate() + 1);
61120
// Handle month boundary
62-
if (nextRun.getDate() === 1 && !config.daysOfMonth.includes(1)) {
121+
if (nextDate.getDate() === 1 && !config.daysOfMonth.includes(1)) {
63122
// We've rolled over to next month, check if we need to skip ahead
64123
const maxDay = Math.max(...config.daysOfMonth);
65-
if (maxDay > new Date(nextRun.getFullYear(), nextRun.getMonth() + 1, 0).getDate()) {
124+
if (maxDay > new Date(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate()) {
66125
// Skip this month if our target day doesn't exist
67-
nextRun.setMonth(nextRun.getMonth() + 1);
126+
nextDate.setMonth(nextDate.getMonth() + 1);
68127
}
69128
}
129+
nextRun = createScheduledDate(nextDate);
70130
}
71131
}
72132

src/components/ui/toast.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
1414
<ToastPrimitives.Viewport
1515
ref={ref}
1616
className={cn(
17-
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
17+
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:left-0 sm:top-auto sm:flex-col md:max-w-[420px]",
1818
className
1919
)}
2020
{...props}

0 commit comments

Comments
 (0)