feat: manage current date with datetime api. Closes #6
This commit is contained in:
+59
@@ -393,6 +393,65 @@
|
||||
<label for="bmiResult">Body Mass Index (BMI) Result</label>
|
||||
<input type="text" id="bmiResult" placeholder="BMI Result" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="timezone">Timezone</label>
|
||||
<select id="timezone">
|
||||
<option value="">— Use browser time —</option>
|
||||
<optgroup label="UTC">
|
||||
<option value="UTC">UTC</option>
|
||||
</optgroup>
|
||||
<optgroup label="Americas">
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="America/Chicago">America/Chicago</option>
|
||||
<option value="America/Denver">America/Denver</option>
|
||||
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
||||
<option value="America/Toronto">America/Toronto</option>
|
||||
<option value="America/Vancouver">America/Vancouver</option>
|
||||
<option value="America/Mexico_City">America/Mexico_City</option>
|
||||
<option value="America/Bogota">America/Bogota</option>
|
||||
<option value="America/Lima">America/Lima</option>
|
||||
<option value="America/Caracas">America/Caracas</option>
|
||||
<option value="America/La_Paz">America/La_Paz</option>
|
||||
<option value="America/Santiago">America/Santiago</option>
|
||||
<option value="America/Sao_Paulo">America/Sao_Paulo</option>
|
||||
<option value="America/Argentina/Buenos_Aires">America/Argentina/Buenos_Aires</option>
|
||||
</optgroup>
|
||||
<optgroup label="Europe">
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
<option value="Europe/Lisbon">Europe/Lisbon</option>
|
||||
<option value="Europe/Amsterdam">Europe/Amsterdam</option>
|
||||
<option value="Europe/Berlin">Europe/Berlin</option>
|
||||
<option value="Europe/Madrid">Europe/Madrid</option>
|
||||
<option value="Europe/Paris">Europe/Paris</option>
|
||||
<option value="Europe/Rome">Europe/Rome</option>
|
||||
<option value="Europe/Helsinki">Europe/Helsinki</option>
|
||||
<option value="Europe/Moscow">Europe/Moscow</option>
|
||||
<option value="Europe/Istanbul">Europe/Istanbul</option>
|
||||
</optgroup>
|
||||
<optgroup label="Africa">
|
||||
<option value="Africa/Cairo">Africa/Cairo</option>
|
||||
<option value="Africa/Lagos">Africa/Lagos</option>
|
||||
<option value="Africa/Nairobi">Africa/Nairobi</option>
|
||||
<option value="Africa/Johannesburg">Africa/Johannesburg</option>
|
||||
</optgroup>
|
||||
<optgroup label="Asia">
|
||||
<option value="Asia/Karachi">Asia/Karachi</option>
|
||||
<option value="Asia/Kolkata">Asia/Kolkata</option>
|
||||
<option value="Asia/Dhaka">Asia/Dhaka</option>
|
||||
<option value="Asia/Bangkok">Asia/Bangkok</option>
|
||||
<option value="Asia/Jakarta">Asia/Jakarta</option>
|
||||
<option value="Asia/Singapore">Asia/Singapore</option>
|
||||
<option value="Asia/Shanghai">Asia/Shanghai</option>
|
||||
<option value="Asia/Seoul">Asia/Seoul</option>
|
||||
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||||
<option value="Asia/Dubai">Asia/Dubai</option>
|
||||
</optgroup>
|
||||
<optgroup label="Oceania">
|
||||
<option value="Australia/Sydney">Australia/Sydney</option>
|
||||
<option value="Pacific/Auckland">Pacific/Auckland</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="settings-btn">Save</button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -32,6 +32,35 @@
|
||||
border-color: #6B8DD6;
|
||||
}
|
||||
|
||||
.settings-form select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
padding-right: 36px;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #404040;
|
||||
background-color: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23a0a0a0' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-form select:focus {
|
||||
outline: none;
|
||||
border-color: #6B8DD6;
|
||||
}
|
||||
|
||||
.settings-form select option,
|
||||
.settings-form select optgroup {
|
||||
background-color: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
export interface CurrentDateResult {
|
||||
date: string; // "2026-02-13"
|
||||
dateTime: Date; // new Date(year, month-1, day) — local-midnight Date
|
||||
timeString: string; // "HH:MM"
|
||||
}
|
||||
|
||||
export async function getCurrentDate(timezone: string): Promise<CurrentDateResult> {
|
||||
if (timezone) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://timeapi.io/api/v1/time/current/zone?timezone=${encodeURIComponent(timezone)}`
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// data.date is "YYYY-MM-DD", data.time is "HH:MM:SS.mmm" or "HH:MM"
|
||||
const [year, month, day] = data.date.split('-').map(Number);
|
||||
const dateTime = new Date(year, month - 1, day);
|
||||
const timeParts = data.time.split(':');
|
||||
const timeString = `${timeParts[0].padStart(2, '0')}:${timeParts[1].padStart(2, '0')}`;
|
||||
return { date: data.date, dateTime, timeString };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('timeapi.io request failed, falling back to browser time:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: browser local time
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth() + 1;
|
||||
const day = now.getDate();
|
||||
const date = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const dateTime = new Date(year, now.getMonth(), day);
|
||||
const timeString = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||
return { date, dateTime, timeString };
|
||||
}
|
||||
+25
-6
@@ -7,6 +7,7 @@ import { closeMobileMenu, delay, getCleanName, getIcon, handleMobileCalendarClic
|
||||
import { showAuthForms, toggleAuthForms, showRegisterForm, showLoginForm, hideAuthForms, closeAuthModal } from './auth';
|
||||
import { appState } from "./state";
|
||||
import { getNutritionInfo, NutritionInfo, clearNutritionCache, getCacheStats } from './claudeService';
|
||||
import { getCurrentDate } from './dateUtils';
|
||||
|
||||
// PWA Service Worker Registration
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
@@ -42,6 +43,20 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
async function initTimezone() {
|
||||
try {
|
||||
const allDocs = await AppwriteDB.getUserSettings();
|
||||
const globalSettings = allDocs.find((d: any) => !d.goalName);
|
||||
appState.userTimezone = globalSettings?.timezone ?? '';
|
||||
} catch {
|
||||
appState.userTimezone = '';
|
||||
}
|
||||
const result = await getCurrentDate(appState.userTimezone);
|
||||
selectedDate = result.dateTime;
|
||||
currentViewDate = result.dateTime;
|
||||
appState.todayDateString = result.date;
|
||||
}
|
||||
|
||||
async function initializeAuth() {
|
||||
// Check if this is a shared view first
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -60,6 +75,7 @@ async function initializeAuth() {
|
||||
if (isLoggedIn) {
|
||||
currentUser = await AppwriteAuth.getCurrentUser();
|
||||
await showMainApp();
|
||||
await initTimezone();
|
||||
selectDate(selectedDate);
|
||||
} else {
|
||||
showAuthForms();
|
||||
@@ -91,6 +107,7 @@ async function handleRegister(e: SubmitEvent) {
|
||||
|
||||
closeAuthModal();
|
||||
await showMainApp();
|
||||
await initTimezone();
|
||||
selectDate(selectedDate);
|
||||
|
||||
swal('Registration successful! Welcome to Food Tracker.');
|
||||
@@ -117,6 +134,7 @@ async function handleLogin(e: SubmitEvent) {
|
||||
|
||||
closeAuthModal();
|
||||
await showMainApp();
|
||||
await initTimezone();
|
||||
selectDate(selectedDate);
|
||||
|
||||
} catch (error) {
|
||||
@@ -192,12 +210,14 @@ async function handleSaveSettings(e: SubmitEvent) {
|
||||
const height = getInputById('height').value;
|
||||
const bmi = getInputById('bmi').value;
|
||||
const bmiResult = getInputById('bmiResult');
|
||||
const timezone = getInputById('timezone').value;
|
||||
|
||||
const metricsData = {
|
||||
bodyWeight: bodyWeight ? parseFloat(bodyWeight) : undefined,
|
||||
height: height ? parseFloat(height) : undefined,
|
||||
bmi: bmi ? parseFloat(bmi) : undefined,
|
||||
bmiResult: bmiResult ? bmiResult.value : undefined,
|
||||
timezone: timezone || undefined,
|
||||
};
|
||||
|
||||
// Find existing global settings doc (no goalName) and update it, or create new one
|
||||
@@ -210,6 +230,7 @@ async function handleSaveSettings(e: SubmitEvent) {
|
||||
await AppwriteDB.saveUserSettings(metricsData);
|
||||
}
|
||||
|
||||
appState.userTimezone = timezone;
|
||||
hideLoading();
|
||||
toggleSettingsView();
|
||||
selectDate(selectedDate);
|
||||
@@ -308,11 +329,13 @@ async function toggleSettingsView() {
|
||||
getInputById('height').value = globalSettings.height ?? '';
|
||||
getInputById('bmi').value = globalSettings.bmi ?? '';
|
||||
getInputById('bmiResult').value = globalSettings.bmiResult ?? '';
|
||||
(document.getElementById('timezone') as HTMLSelectElement).value = globalSettings.timezone ?? '';
|
||||
} else {
|
||||
getInputById('bodyWeight').value = '';
|
||||
getInputById('height').value = '';
|
||||
getInputById('bmi').value = '';
|
||||
getInputById('bmiResult').value = '';
|
||||
(document.getElementById('timezone') as HTMLSelectElement).value = '';
|
||||
}
|
||||
|
||||
const goals: UserSettings[] = allDocs
|
||||
@@ -1331,11 +1354,7 @@ const addFood = async () => {
|
||||
fiber: proportion.info.fiber,
|
||||
date: date.split('T')[0],
|
||||
alkaline: selectedFood.info.alkaline,
|
||||
time: new Date().toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
time: (await getCurrentDate(appState.userTimezone)).timeString
|
||||
};
|
||||
|
||||
// Save to Appwrite
|
||||
@@ -1961,7 +1980,7 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont
|
||||
}
|
||||
|
||||
// Check if this is today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = appState.todayDateString || new Date().toISOString().split('T')[0];
|
||||
if (!isOtherMonth && dateString === today) {
|
||||
dayElement.classList.add('today');
|
||||
}
|
||||
|
||||
+5
-1
@@ -7,6 +7,8 @@ interface AppState {
|
||||
calendarMonthlyCalories: DailyTotalCalories[];
|
||||
isSharedView: boolean;
|
||||
sharedData: SharedDay | null;
|
||||
userTimezone: string;
|
||||
todayDateString: string;
|
||||
}
|
||||
|
||||
export const appState: AppState = {
|
||||
@@ -15,5 +17,7 @@ export const appState: AppState = {
|
||||
searchResults: [],
|
||||
calendarMonthlyCalories: [],
|
||||
isSharedView: false,
|
||||
sharedData: null
|
||||
sharedData: null,
|
||||
userTimezone: '',
|
||||
todayDateString: ''
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ export type UserSettings = {
|
||||
bmiResult?: string;
|
||||
goalName?: string;
|
||||
isActive?: boolean;
|
||||
timezone?: string;
|
||||
};
|
||||
|
||||
export type DailyTotalCalories = {
|
||||
|
||||
Reference in New Issue
Block a user