fix(timezone): all date/time display now uses IST (Asia/Kolkata)
New src/lib/date-ist.ts utility: - hourIST() — current hour in IST (replaces new Date().getHours() on server) - isTodayIST() / dateIST() — IST-aware "today" comparisons - fmtTime() — time display with timeZone: "Asia/Kolkata" - fmtDate() — date display with timeZone: "Asia/Kolkata" - dayLabel() — "Today" / "Yesterday" / "Mon, 26 May" in IST Applied across: home (greeting, today-summary, log times), activity (day grouping, bar chart, log times), ai, growth, milestones, settings — eliminating Finland-timezone artifacts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
774e8f29d4
commit
309fd5aa29
7 changed files with 118 additions and 31 deletions
|
|
@ -8,6 +8,7 @@ import { api } from "@/lib/api";
|
||||||
import { CalendarView } from "@/components/CalendarView";
|
import { CalendarView } from "@/components/CalendarView";
|
||||||
import { LogModal, type LogType as ModalLogType, type SmartDefault } from "@/components/LogModal";
|
import { LogModal, type LogType as ModalLogType, type SmartDefault } from "@/components/LogModal";
|
||||||
import type { Log, LogType } from "@/types";
|
import type { Log, LogType } from "@/types";
|
||||||
|
import { dateIST, todayIST, fmtTime, fmtDate, dayLabel } from "@/lib/date-ist";
|
||||||
|
|
||||||
type ViewMode = "timeline" | "calendar";
|
type ViewMode = "timeline" | "calendar";
|
||||||
|
|
||||||
|
|
@ -23,13 +24,9 @@ function getIcon(type: LogType) {
|
||||||
return "📝";
|
return "📝";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dateStr is now an IST YYYY-MM-DD string (from dateIST())
|
||||||
function formatDayLabel(dateStr: string): string {
|
function formatDayLabel(dateStr: string): string {
|
||||||
const d = new Date(dateStr);
|
return dayLabel(dateStr);
|
||||||
const today = new Date().toDateString();
|
|
||||||
const yesterday = new Date(Date.now() - 86_400_000).toDateString();
|
|
||||||
if (d.toDateString() === today) return "Today";
|
|
||||||
if (d.toDateString() === yesterday) return "Yesterday";
|
|
||||||
return d.toLocaleDateString("en-IN", { weekday: "short", month: "short", day: "numeric" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ActivityPage() {
|
export default function ActivityPage() {
|
||||||
|
|
@ -100,8 +97,8 @@ export default function ActivityPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Today's stats (computed from loaded logs — no extra fetch)
|
// Today's stats (computed from loaded logs — no extra fetch)
|
||||||
const todayStr = new Date().toDateString();
|
const todayStr = todayIST();
|
||||||
const todayLogs = logs.filter(l => new Date(l.loggedAt).toDateString() === todayStr);
|
const todayLogs = logs.filter(l => dateIST(l.loggedAt) === todayStr);
|
||||||
const todayCounts = {
|
const todayCounts = {
|
||||||
feed: todayLogs.filter(l => l.type === "feed").length,
|
feed: todayLogs.filter(l => l.type === "feed").length,
|
||||||
sleep: todayLogs.filter(l => l.type === "sleep").length,
|
sleep: todayLogs.filter(l => l.type === "sleep").length,
|
||||||
|
|
@ -111,21 +108,21 @@ export default function ActivityPage() {
|
||||||
// Last 4 calendar days (computed from loaded logs — no extra fetch)
|
// Last 4 calendar days (computed from loaded logs — no extra fetch)
|
||||||
const last4Days = Array.from({ length: 4 }, (_, i) => {
|
const last4Days = Array.from({ length: 4 }, (_, i) => {
|
||||||
const d = new Date(Date.now() - i * 86_400_000);
|
const d = new Date(Date.now() - i * 86_400_000);
|
||||||
const dateStr = d.toDateString();
|
const dateStr = dateIST(d);
|
||||||
const dayLogs = logs.filter(l => new Date(l.loggedAt).toDateString() === dateStr);
|
const dLogs = logs.filter(l => dateIST(l.loggedAt) === dateStr);
|
||||||
return {
|
return {
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
label: i === 0 ? "Today" : i === 1 ? "Yest." : d.toLocaleDateString("en-IN", { weekday: "short" }),
|
label: i === 0 ? "Today" : i === 1 ? "Yest." : fmtDate(d, { weekday: "short" }),
|
||||||
feed: dayLogs.filter(l => l.type === "feed").length,
|
feed: dLogs.filter(l => l.type === "feed").length,
|
||||||
sleep: dayLogs.filter(l => l.type === "sleep").length,
|
sleep: dLogs.filter(l => l.type === "sleep").length,
|
||||||
diaper: dayLogs.filter(l => l.type === "diaper").length,
|
diaper: dLogs.filter(l => l.type === "diaper").length,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredLogs = filter === "all" ? logs : logs.filter(l => l.type === filter);
|
const filteredLogs = filter === "all" ? logs : logs.filter(l => l.type === filter);
|
||||||
|
|
||||||
const groupedByDay = filteredLogs.reduce<DayLogs[]>((acc, log) => {
|
const groupedByDay = filteredLogs.reduce<DayLogs[]>((acc, log) => {
|
||||||
const date = new Date(log.loggedAt).toDateString();
|
const date = dateIST(log.loggedAt);
|
||||||
const existing = acc.find(d => d.date === date);
|
const existing = acc.find(d => d.date === date);
|
||||||
if (existing) existing.logs.push(log);
|
if (existing) existing.logs.push(log);
|
||||||
else acc.push({ date, logs: [log] });
|
else acc.push({ date, logs: [log] });
|
||||||
|
|
@ -359,7 +356,7 @@ export default function ActivityPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||||
<span className="text-sm text-gray-400">
|
<span className="text-sm text-gray-400">
|
||||||
{new Date(log.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
{fmtTime(log.loggedAt)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-300 dark:text-gray-600 group-hover:text-rose-400 transition-colors text-base leading-none">›</span>
|
<span className="text-gray-300 dark:text-gray-600 group-hover:text-rose-400 transition-colors text-base leading-none">›</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -413,7 +410,7 @@ export default function ActivityPage() {
|
||||||
{[
|
{[
|
||||||
selectedLog.subType?.replace(/_/g, " "),
|
selectedLog.subType?.replace(/_/g, " "),
|
||||||
selectedLog.amount ? `${selectedLog.amount}ml` : null,
|
selectedLog.amount ? `${selectedLog.amount}ml` : null,
|
||||||
new Date(selectedLog.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
fmtTime(selectedLog.loggedAt),
|
||||||
].filter(Boolean).join(" · ")}
|
].filter(Boolean).join(" · ")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -473,7 +470,7 @@ export default function ActivityPage() {
|
||||||
const { date: sheetDateStr, type: sheetType } = daySheetDate;
|
const { date: sheetDateStr, type: sheetType } = daySheetDate;
|
||||||
const sheetLogs = logs
|
const sheetLogs = logs
|
||||||
.filter(l =>
|
.filter(l =>
|
||||||
new Date(l.loggedAt).toDateString() === sheetDateStr &&
|
dateIST(l.loggedAt) === sheetDateStr &&
|
||||||
l.type === sheetType
|
l.type === sheetType
|
||||||
)
|
)
|
||||||
.sort((a, b) => new Date(b.loggedAt).getTime() - new Date(a.loggedAt).getTime());
|
.sort((a, b) => new Date(b.loggedAt).getTime() - new Date(a.loggedAt).getTime());
|
||||||
|
|
@ -538,7 +535,7 @@ export default function ActivityPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
<span className="text-sm text-gray-400">
|
<span className="text-sm text-gray-400">
|
||||||
{new Date(log.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
{fmtTime(log.loggedAt)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-300 dark:text-gray-600 group-hover:text-rose-400 transition-colors text-lg">›</span>
|
<span className="text-gray-300 dark:text-gray-600 group-hover:text-rose-400 transition-colors text-lg">›</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
|
||||||
import { useFamily } from "@/app/FamilyProvider";
|
import { useFamily } from "@/app/FamilyProvider";
|
||||||
import { Button, Input, ConfirmDialog } from "@/components/ui";
|
import { Button, Input, ConfirmDialog } from "@/components/ui";
|
||||||
import type { AIChat, ChatSession } from "@/types";
|
import type { AIChat, ChatSession } from "@/types";
|
||||||
|
import { fmtDate } from "@/lib/date-ist";
|
||||||
|
|
||||||
export default function AIChatPage() {
|
export default function AIChatPage() {
|
||||||
const { childId } = useFamily();
|
const { childId } = useFamily();
|
||||||
|
|
@ -186,7 +187,7 @@ export default function AIChatPage() {
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm font-medium truncate dark:text-gray-100">{session.title}</div>
|
<div className="text-sm font-medium truncate dark:text-gray-100">{session.title}</div>
|
||||||
<div className="text-xs text-gray-400 dark:text-gray-500">{new Date(session.updatedAt).toLocaleDateString()}</div>
|
<div className="text-xs text-gray-400 dark:text-gray-500">{fmtDate(session.updatedAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setDeleteConfirm(session.id); }}
|
onClick={(e) => { e.stopPropagation(); setDeleteConfirm(session.id); }}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useFamily } from "@/app/FamilyProvider";
|
||||||
import { Button, Card, Input, ConfirmDialog } from "@/components/ui";
|
import { Button, Card, Input, ConfirmDialog } from "@/components/ui";
|
||||||
import { WHO_BOY_WEIGHT, WHO_GIRL_WEIGHT, getAgeInMonthsFromBirth, getPercentile } from "@/lib/growth-standards";
|
import { WHO_BOY_WEIGHT, WHO_GIRL_WEIGHT, getAgeInMonthsFromBirth, getPercentile } from "@/lib/growth-standards";
|
||||||
import { formatAge } from "@/lib/formatting";
|
import { formatAge } from "@/lib/formatting";
|
||||||
|
import { fmtDate } from "@/lib/date-ist";
|
||||||
import type { GrowthRecord, Goal } from "@/types";
|
import type { GrowthRecord, Goal } from "@/types";
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
|
|
@ -190,7 +191,7 @@ export default function GrowthPage() {
|
||||||
|
|
||||||
const headers = ["Date", "Weight (kg)", "Height (cm)", "Head (cm)", "Notes"];
|
const headers = ["Date", "Weight (kg)", "Height (cm)", "Head (cm)", "Notes"];
|
||||||
const rows = growthData.map(r => [
|
const rows = growthData.map(r => [
|
||||||
new Date(r.measured_at).toLocaleDateString(),
|
fmtDate(r.measured_at),
|
||||||
r.weight_kg || "",
|
r.weight_kg || "",
|
||||||
r.height_cm || "",
|
r.height_cm || "",
|
||||||
r.head_circumference_cm || "",
|
r.head_circumference_cm || "",
|
||||||
|
|
@ -423,7 +424,7 @@ export default function GrowthPage() {
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-rose-600 dark:text-rose-300">Latest Reading</div>
|
<div className="font-semibold text-rose-600 dark:text-rose-300">Latest Reading</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{new Date(latest.measured_at).toLocaleDateString()} ({child ? formatAge(child.birthDate, latest.measured_at) : ""})
|
{fmtDate(latest.measured_at)} ({child ? formatAge(child.birthDate, latest.measured_at) : ""})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{velocity && (
|
{velocity && (
|
||||||
|
|
@ -637,7 +638,7 @@ export default function GrowthPage() {
|
||||||
<div key={i} className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg hover:shadow-md transition-shadow flex justify-between items-center">
|
<div key={i} className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg hover:shadow-md transition-shadow flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{new Date(record.measured_at).toLocaleDateString()} ({child ? formatAge(child.birthDate, record.measured_at) : ""})
|
{fmtDate(record.measured_at)} ({child ? formatAge(child.birthDate, record.measured_at) : ""})
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 mt-1">
|
<div className="flex gap-2 mt-1">
|
||||||
{record.weight_kg && (
|
{record.weight_kg && (
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useStageCheck, type BabyStage } from "@/hooks/useStageCheck";
|
||||||
import { LogModal, type LogType } from "@/components/LogModal";
|
import { LogModal, type LogType } from "@/components/LogModal";
|
||||||
import { getOfflineQueue, processOfflineQueue } from "@/lib/offline-queue";
|
import { getOfflineQueue, processOfflineQueue } from "@/lib/offline-queue";
|
||||||
import { calculateAge, formatTimeAgo } from "@/lib/formatting";
|
import { calculateAge, formatTimeAgo } from "@/lib/formatting";
|
||||||
|
import { hourIST, isTodayIST, fmtTime } from "@/lib/date-ist";
|
||||||
import type { Log, AIChat, ChatSession } from "@/types";
|
import type { Log, AIChat, ChatSession } from "@/types";
|
||||||
|
|
||||||
async function getSessions(cid: string): Promise<ChatSession[]> {
|
async function getSessions(cid: string): Promise<ChatSession[]> {
|
||||||
|
|
@ -32,15 +33,14 @@ async function createSession(cid: string): Promise<ChatSession | null> {
|
||||||
|
|
||||||
|
|
||||||
function getGreeting() {
|
function getGreeting() {
|
||||||
const hour = new Date().getHours();
|
const hour = hourIST();
|
||||||
if (hour < 12) return "Good morning";
|
if (hour < 12) return "Good morning";
|
||||||
if (hour < 18) return "Good afternoon";
|
if (hour < 18) return "Good afternoon";
|
||||||
return "Good evening";
|
return "Good evening";
|
||||||
}
|
}
|
||||||
|
|
||||||
function TodaySummary({ logs }: { logs: Log[] }) {
|
function TodaySummary({ logs }: { logs: Log[] }) {
|
||||||
const todayStr = new Date().toDateString();
|
const today = logs.filter(l => isTodayIST(l.loggedAt));
|
||||||
const today = logs.filter(l => new Date(l.loggedAt).toDateString() === todayStr);
|
|
||||||
const counts = {
|
const counts = {
|
||||||
feed: today.filter(l => l.type === "feed").length,
|
feed: today.filter(l => l.type === "feed").length,
|
||||||
diaper: today.filter(l => l.type === "diaper").length,
|
diaper: today.filter(l => l.type === "diaper").length,
|
||||||
|
|
@ -400,7 +400,7 @@ export default function HomePage() {
|
||||||
<TodaySummary logs={recentLogs} />
|
<TodaySummary logs={recentLogs} />
|
||||||
|
|
||||||
{stage && (() => {
|
{stage && (() => {
|
||||||
const h = new Date().getHours();
|
const h = hourIST();
|
||||||
type Suggestion = { label: string; type: "feed" | "sleep" | "diaper" };
|
type Suggestion = { label: string; type: "feed" | "sleep" | "diaper" };
|
||||||
const matrix: Record<BabyStage, Suggestion> =
|
const matrix: Record<BabyStage, Suggestion> =
|
||||||
h >= 5 && h < 9 ? { newborn: { label: "Feed", type: "feed" }, infant: { label: "Feed", type: "feed" }, sitter: { label: "Feed", type: "feed" }, crawler: { label: "Feed", type: "feed" }, toddler: { label: "Feed", type: "feed" }, walker: { label: "Feed", type: "feed" } } :
|
h >= 5 && h < 9 ? { newborn: { label: "Feed", type: "feed" }, infant: { label: "Feed", type: "feed" }, sitter: { label: "Feed", type: "feed" }, crawler: { label: "Feed", type: "feed" }, toddler: { label: "Feed", type: "feed" }, walker: { label: "Feed", type: "feed" } } :
|
||||||
|
|
@ -465,7 +465,7 @@ export default function HomePage() {
|
||||||
<div key={log.id} className="flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-xl">
|
<div key={log.id} className="flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-xl">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xl">{log.type === "feed" && "🍼"}{log.type === "sleep" && "😴"}{log.type === "diaper" && "🚼"}</span>
|
<span className="text-xl">{log.type === "feed" && "🍼"}{log.type === "sleep" && "😴"}{log.type === "diaper" && "🚼"}</span>
|
||||||
<div><div className="font-medium capitalize">{log.type}</div><div className="text-xs text-gray-500 dark:text-gray-400">{new Date(log.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div></div>
|
<div><div className="font-medium capitalize">{log.type}</div><div className="text-xs text-gray-500 dark:text-gray-400">{fmtTime(log.loggedAt)}</div></div>
|
||||||
</div>
|
</div>
|
||||||
{log.amount && <span className="text-sm text-gray-500 dark:text-gray-400">{log.amount}ml</span>}
|
{log.amount && <span className="text-sm text-gray-500 dark:text-gray-400">{log.amount}ml</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useFamily } from "@/app/FamilyProvider";
|
||||||
import { useStageCheck } from "@/hooks/useStageCheck";
|
import { useStageCheck } from "@/hooks/useStageCheck";
|
||||||
import { MILESTONES, type MilestoneDef } from "@/lib/milestones";
|
import { MILESTONES, type MilestoneDef } from "@/lib/milestones";
|
||||||
import { Button, Input } from "@/components/ui";
|
import { Button, Input } from "@/components/ui";
|
||||||
|
import { fmtDate } from "@/lib/date-ist";
|
||||||
|
|
||||||
type Category = "all" | "social" | "motor" | "language" | "cognitive";
|
type Category = "all" | "social" | "motor" | "language" | "cognitive";
|
||||||
type Filter = "all" | "achieved" | "upcoming";
|
type Filter = "all" | "achieved" | "upcoming";
|
||||||
|
|
@ -194,7 +195,7 @@ export default function MilestonesPage() {
|
||||||
<p className="text-xs text-gray-400 mt-1">{m.ageRangeLabel}</p>
|
<p className="text-xs text-gray-400 mt-1">{m.ageRangeLabel}</p>
|
||||||
{m.achieved && m.achievedAt && (
|
{m.achieved && m.achievedAt && (
|
||||||
<p className="text-xs text-green-600 dark:text-green-400 mt-1">
|
<p className="text-xs text-green-600 dark:text-green-400 mt-1">
|
||||||
{new Date(m.achievedAt).toLocaleDateString("en-IN", { day: "numeric", month: "short" })}
|
{fmtDate(m.achievedAt, { day: "numeric", month: "short" })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<span className={`inline-block text-xs px-1.5 py-0.5 rounded-full mt-2 ${CATEGORY_COLORS[m.category]}`}>
|
<span className={`inline-block text-xs px-1.5 py-0.5 rounded-full mt-2 ${CATEGORY_COLORS[m.category]}`}>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { useTheme } from "@/app/ThemeProvider";
|
||||||
import { useFamily } from "@/app/FamilyProvider";
|
import { useFamily } from "@/app/FamilyProvider";
|
||||||
import { Button, Card, Input, Select, Badge } from "@/components/ui";
|
import { Button, Card, Input, Select, Badge } from "@/components/ui";
|
||||||
import { StorageMeter, MemberLimitBanner } from "@/components/StorageMeter";
|
import { StorageMeter, MemberLimitBanner } from "@/components/StorageMeter";
|
||||||
|
import { fmtDate } from "@/lib/date-ist";
|
||||||
|
|
||||||
interface Member {
|
interface Member {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -81,7 +82,7 @@ export default function SettingsPage() {
|
||||||
if (records.length === 0) { alert("No growth records to export."); return; }
|
if (records.length === 0) { alert("No growth records to export."); return; }
|
||||||
const headers = ["Date", "Weight (kg)", "Height (cm)", "Head (cm)", "Notes"];
|
const headers = ["Date", "Weight (kg)", "Height (cm)", "Head (cm)", "Notes"];
|
||||||
const rows = records.map((r: { measured_at: string; weight_kg: number | null; height_cm: number | null; head_circumference_cm: number | null; notes: string | null }) => [
|
const rows = records.map((r: { measured_at: string; weight_kg: number | null; height_cm: number | null; head_circumference_cm: number | null; notes: string | null }) => [
|
||||||
new Date(r.measured_at).toLocaleDateString(),
|
fmtDate(r.measured_at),
|
||||||
r.weight_kg ?? "",
|
r.weight_kg ?? "",
|
||||||
r.height_cm ?? "",
|
r.height_cm ?? "",
|
||||||
r.head_circumference_cm ?? "",
|
r.head_circumference_cm ?? "",
|
||||||
|
|
@ -315,7 +316,7 @@ export default function SettingsPage() {
|
||||||
<div key={invite.id} className="flex justify-between items-center p-2 bg-gray-50 dark:bg-gray-700 rounded text-sm mb-1">
|
<div key={invite.id} className="flex justify-between items-center p-2 bg-gray-50 dark:bg-gray-700 rounded text-sm mb-1">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-800 dark:text-gray-100">{invite.email}</div>
|
<div className="font-medium text-gray-800 dark:text-gray-100">{invite.email}</div>
|
||||||
<div className="text-xs text-gray-400">Pending · expires {new Date(invite.expiresAt).toLocaleDateString()}</div>
|
<div className="text-xs text-gray-400">Pending · expires {fmtDate(invite.expiresAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => deleteInvite(invite.id)}
|
onClick={() => deleteInvite(invite.id)}
|
||||||
|
|
|
||||||
86
src/lib/date-ist.ts
Normal file
86
src/lib/date-ist.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* date-ist.ts — IST-aware date/time helpers.
|
||||||
|
*
|
||||||
|
* All functions explicitly pass timeZone: "Asia/Kolkata" so they produce
|
||||||
|
* consistent IST output regardless of where they run — Next.js SSR on a
|
||||||
|
* European server OR the user's browser.
|
||||||
|
*
|
||||||
|
* Never use new Date().toDateString(), .getHours(), or .toLocaleString()
|
||||||
|
* without a timeZone option — those use the host's local timezone.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TZ = "Asia/Kolkata";
|
||||||
|
const LOCALE = "en-IN";
|
||||||
|
|
||||||
|
/** ISO date string (YYYY-MM-DD) for today in IST — use for comparisons. */
|
||||||
|
export function todayIST(): string {
|
||||||
|
// "sv-SE" locale gives YYYY-MM-DD — a clean, comparable format.
|
||||||
|
return new Date().toLocaleDateString("sv-SE", { timeZone: TZ });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ISO date string (YYYY-MM-DD) for any timestamp, evaluated in IST. */
|
||||||
|
export function dateIST(isoOrDate: string | Date): string {
|
||||||
|
return new Date(isoOrDate).toLocaleDateString("sv-SE", { timeZone: TZ });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if the given timestamp falls on today in IST. */
|
||||||
|
export function isTodayIST(isoOrDate: string | Date): boolean {
|
||||||
|
return dateIST(isoOrDate) === todayIST();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ISO date string N calendar days before today in IST. */
|
||||||
|
function nDaysAgoIST(n: number): string {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() - n);
|
||||||
|
return d.toLocaleDateString("sv-SE", { timeZone: TZ });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Current hour 0-23 in IST. Replaces new Date().getHours() on the server. */
|
||||||
|
export function hourIST(): number {
|
||||||
|
// Intl gives a locale-formatted hour string; parseInt handles "24" edge case.
|
||||||
|
const str = new Intl.DateTimeFormat(LOCALE, {
|
||||||
|
timeZone: TZ,
|
||||||
|
hour: "numeric",
|
||||||
|
hour12: false,
|
||||||
|
}).format(new Date());
|
||||||
|
const h = parseInt(str, 10);
|
||||||
|
return h === 24 ? 0 : h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format a timestamp as "hh:mm AM/PM" in IST. */
|
||||||
|
export function fmtTime(isoOrDate: string | Date): string {
|
||||||
|
return new Date(isoOrDate).toLocaleTimeString(LOCALE, {
|
||||||
|
timeZone: TZ,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format a timestamp as a date string in IST with given Intl options. */
|
||||||
|
export function fmtDate(
|
||||||
|
isoOrDate: string | Date,
|
||||||
|
opts: Intl.DateTimeFormatOptions = { day: "numeric", month: "short", year: "numeric" }
|
||||||
|
): string {
|
||||||
|
return new Date(isoOrDate).toLocaleDateString(LOCALE, { timeZone: TZ, ...opts });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Today" | "Yesterday" | "Mon, 26 May" — for activity group headers.
|
||||||
|
* Replaces the toDateString()-based pattern throughout the app.
|
||||||
|
*/
|
||||||
|
export function dayLabel(isoOrDate: string | Date): string {
|
||||||
|
const d = dateIST(isoOrDate);
|
||||||
|
if (d === todayIST()) return "Today";
|
||||||
|
if (d === nDaysAgoIST(1)) return "Yesterday";
|
||||||
|
return fmtDate(isoOrDate, { weekday: "short", month: "short", day: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like dayLabel but short ("Today" | "Yest." | "Mon").
|
||||||
|
* Used in the activity page bar chart labels.
|
||||||
|
*/
|
||||||
|
export function shortDayLabel(isoOrDate: string | Date, index: number): string {
|
||||||
|
if (index === 0) return "Today";
|
||||||
|
if (index === 1) return "Yest.";
|
||||||
|
return fmtDate(isoOrDate, { weekday: "short" });
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue