From eabffdfc56273b7dd48886028c2bb33ffa90d8b2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:11:59 +0000 Subject: [PATCH 1/3] Add timezone converter utility for remote team scheduling - Add timezone converter page with time/date input and multi-timezone display - Add utility functions for timezone conversion using Intl API - Add SEO component with documentation and code examples - Add tool to tools-list for homepage navigation This utility helps remote software teams schedule syncs across timezones by converting a selected time to multiple common timezones simultaneously. Co-Authored-By: dani@jam.dev --- components/seo/TimezoneConverterSEO.tsx | 126 +++++++++++++ components/utils/timezone-converter.utils.ts | 158 ++++++++++++++++ components/utils/tools-list.ts | 6 + pages/utilities/timezone-converter.tsx | 186 +++++++++++++++++++ 4 files changed, 476 insertions(+) create mode 100644 components/seo/TimezoneConverterSEO.tsx create mode 100644 components/utils/timezone-converter.utils.ts create mode 100644 pages/utilities/timezone-converter.tsx diff --git a/components/seo/TimezoneConverterSEO.tsx b/components/seo/TimezoneConverterSEO.tsx new file mode 100644 index 0000000..91e5753 --- /dev/null +++ b/components/seo/TimezoneConverterSEO.tsx @@ -0,0 +1,126 @@ +import CodeExample from "../CodeExample"; + +export default function TimezoneConverterSEO() { + return ( +
+
+

+ Easily convert times between different timezones with this free tool. + Perfect for remote software teams who need to schedule syncs, meetings, + and standups across multiple timezones. No more mental math or timezone + confusion. +

+
+ +
+

Features:

+
    +
  • + Instant Conversion:
    Select a time and see it converted + across all major timezones instantly. +
  • +
  • + Common Timezones:
    Pre-configured with the most common + timezones for distributed teams including US, Europe, and Asia. +
  • +
  • + Open Source:
    Made with love by the developers building + Jam. +
  • +
+
+ +
+

How to use the Timezone Converter:

+
    +
  • + Step 1:
    Select your source timezone and enter the time + you want to convert. +
  • +
  • + Step 2:
    View the converted times across all selected + timezones. +
  • +
  • + Step 3:
    Toggle timezones on/off to customize your view. +
  • +
+
+ +
+

Why Use a Timezone Converter?

+

+ Remote teams often span multiple continents and timezones. Scheduling + meetings that work for everyone can be challenging. A timezone converter + helps you: +

+
    +
  • + Schedule Meetings:
    Find times that work across different + regions without manual calculations. +
  • +
  • + Avoid Confusion:
    Eliminate timezone math errors that can + lead to missed meetings. +
  • +
  • + Plan Ahead:
    See how a proposed meeting time affects team + members in different locations. +
  • +
+
+ +
+

Convert Timezones in JavaScript:

+

+ If you need to convert timezones programmatically in your own + applications, here is a code snippet using the Intl API: +

+
+ +
+ {jsCodeExample} +
+ +
+

FAQs:

+
    +
  • + Does this tool handle Daylight Saving Time?
    Yes, the + converter automatically accounts for DST changes in each timezone. +
  • +
  • + What timezones are supported?
    We support all major + timezones including US (PT, MT, CT, ET), Europe (GMT, CET), and Asia + (IST, SGT, JST). +
  • +
  • + Can I use this for scheduling recurring meetings?
    Yes, + simply select the date and time for your meeting and see how it + translates across timezones. +
  • +
+
+
+ ); +} + +const jsCodeExample = `function convertTimezone(date: Date, fromTz: string, toTz: string): string { + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: toTz, + hour: '2-digit', + minute: '2-digit', + hour12: true, + weekday: 'short', + month: 'short', + day: 'numeric', + }); + + return formatter.format(date); +} + +// Example usage: +const nyTime = new Date('2024-01-15T09:00:00'); +console.log(convertTimezone(nyTime, 'America/New_York', 'Europe/London')); +// Output: "Mon, Jan 15, 02:00 PM" +`; diff --git a/components/utils/timezone-converter.utils.ts b/components/utils/timezone-converter.utils.ts new file mode 100644 index 0000000..f5fef77 --- /dev/null +++ b/components/utils/timezone-converter.utils.ts @@ -0,0 +1,158 @@ +export interface TimezoneInfo { + id: string; + label: string; + offset: string; +} + +export const commonTimezones: TimezoneInfo[] = [ + { id: "America/Los_Angeles", label: "Los Angeles (PT)", offset: "" }, + { id: "America/Denver", label: "Denver (MT)", offset: "" }, + { id: "America/Chicago", label: "Chicago (CT)", offset: "" }, + { id: "America/New_York", label: "New York (ET)", offset: "" }, + { id: "America/Sao_Paulo", label: "Sao Paulo (BRT)", offset: "" }, + { id: "Europe/London", label: "London (GMT/BST)", offset: "" }, + { id: "Europe/Paris", label: "Paris (CET)", offset: "" }, + { id: "Europe/Berlin", label: "Berlin (CET)", offset: "" }, + { id: "Asia/Dubai", label: "Dubai (GST)", offset: "" }, + { id: "Asia/Kolkata", label: "India (IST)", offset: "" }, + { id: "Asia/Singapore", label: "Singapore (SGT)", offset: "" }, + { id: "Asia/Tokyo", label: "Tokyo (JST)", offset: "" }, + { id: "Australia/Sydney", label: "Sydney (AEST)", offset: "" }, +]; + +export function getTimezoneOffset(timezone: string, date: Date): string { + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + timeZoneName: "shortOffset", + }); + const parts = formatter.formatToParts(date); + const offsetPart = parts.find((part) => part.type === "timeZoneName"); + return offsetPart?.value || ""; +} + +export function convertTime( + sourceTime: string, + sourceDate: string, + sourceTimezone: string, + targetTimezones: string[] +): { timezone: string; label: string; time: string; date: string }[] { + if (!sourceTime || !sourceDate) { + return []; + } + + const [hours, minutes] = sourceTime.split(":").map(Number); + const [year, month, day] = sourceDate.split("-").map(Number); + + const sourceFormatter = new Intl.DateTimeFormat("en-US", { + timeZone: sourceTimezone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + + const tempDate = new Date(year, month - 1, day, hours, minutes); + const sourceParts = sourceFormatter.formatToParts(tempDate); + + const getPartValue = ( + parts: Intl.DateTimeFormatPart[], + type: Intl.DateTimeFormatPartTypes + ) => parts.find((p) => p.type === type)?.value || ""; + + const sourceYear = parseInt(getPartValue(sourceParts, "year")); + const sourceMonth = parseInt(getPartValue(sourceParts, "month")); + const sourceDay = parseInt(getPartValue(sourceParts, "day")); + const sourceHour = parseInt(getPartValue(sourceParts, "hour")); + const sourceMinute = parseInt(getPartValue(sourceParts, "minute")); + + const utcDate = new Date( + Date.UTC(sourceYear, sourceMonth - 1, sourceDay, sourceHour, sourceMinute) + ); + + const sourceOffset = getTimezoneOffsetMinutes(sourceTimezone, utcDate); + utcDate.setMinutes(utcDate.getMinutes() - sourceOffset); + + return targetTimezones.map((tz) => { + const tzInfo = commonTimezones.find((t) => t.id === tz); + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: tz, + hour: "2-digit", + minute: "2-digit", + hour12: true, + weekday: "short", + month: "short", + day: "numeric", + }); + + const formatted = formatter.format(utcDate); + const parts = formatted.split(", "); + const weekday = parts[0]; + const datePart = parts[1]; + const timePart = parts[2]; + + return { + timezone: tz, + label: tzInfo?.label || tz, + time: timePart, + date: `${weekday}, ${datePart}`, + }; + }); +} + +function getTimezoneOffsetMinutes(timezone: string, date: Date): number { + const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" })); + const tzDate = new Date(date.toLocaleString("en-US", { timeZone: timezone })); + return (tzDate.getTime() - utcDate.getTime()) / 60000; +} + +export function getCurrentTimeInTimezone(timezone: string): { + time: string; + date: string; +} { + const now = new Date(); + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + hour: "2-digit", + minute: "2-digit", + hour12: true, + weekday: "short", + month: "short", + day: "numeric", + }); + + const formatted = formatter.format(now); + const parts = formatted.split(", "); + const weekday = parts[0]; + const datePart = parts[1]; + const timePart = parts[2]; + + return { + time: timePart, + date: `${weekday}, ${datePart}`, + }; +} + +export function formatTimeForInput(date: Date, timezone: string): string { + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + const parts = formatter.formatToParts(date); + const hour = parts.find((p) => p.type === "hour")?.value || "00"; + const minute = parts.find((p) => p.type === "minute")?.value || "00"; + return `${hour}:${minute}`; +} + +export function formatDateForInput(date: Date, timezone: string): string { + const formatter = new Intl.DateTimeFormat("en-CA", { + timeZone: timezone, + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + return formatter.format(date); +} diff --git a/components/utils/tools-list.ts b/components/utils/tools-list.ts index e4c3ada..1ad74d0 100644 --- a/components/utils/tools-list.ts +++ b/components/utils/tools-list.ts @@ -161,4 +161,10 @@ export const tools = [ "Generate cryptographically secure random strings with configurable character sets. Perfect for API keys, tokens, passwords, and secure identifiers.", link: "/utilities/random-string-generator", }, + { + title: "Timezone Converter", + description: + "Convert times between timezones for remote team scheduling. Perfect for distributed software teams coordinating meetings across multiple timezones.", + link: "/utilities/timezone-converter", + }, ]; diff --git a/pages/utilities/timezone-converter.tsx b/pages/utilities/timezone-converter.tsx new file mode 100644 index 0000000..2251c00 --- /dev/null +++ b/pages/utilities/timezone-converter.tsx @@ -0,0 +1,186 @@ +import { useCallback, useState, useEffect } from "react"; +import PageHeader from "@/components/PageHeader"; +import { Card } from "@/components/ds/CardComponent"; +import { Button } from "@/components/ds/ButtonComponent"; +import { Label } from "@/components/ds/LabelComponent"; +import { Input } from "@/components/ds/InputComponent"; +import { Checkbox } from "@/components/ds/CheckboxComponent"; +import Header from "@/components/Header"; +import { CMDK } from "@/components/CMDK"; +import { useCopyToClipboard } from "@/components/hooks/useCopyToClipboard"; +import TimezoneConverterSEO from "@/components/seo/TimezoneConverterSEO"; +import CallToActionGrid from "@/components/CallToActionGrid"; +import Meta from "@/components/Meta"; +import { + commonTimezones, + convertTime, + formatTimeForInput, + formatDateForInput, +} from "@/components/utils/timezone-converter.utils"; + +export default function TimezoneConverter() { + const [sourceTimezone, setSourceTimezone] = useState("America/New_York"); + const [sourceTime, setSourceTime] = useState(""); + const [sourceDate, setSourceDate] = useState(""); + const [selectedTimezones, setSelectedTimezones] = useState( + commonTimezones.map((tz) => tz.id) + ); + const [convertedTimes, setConvertedTimes] = useState< + { timezone: string; label: string; time: string; date: string }[] + >([]); + const { buttonText, handleCopy } = useCopyToClipboard(); + + useEffect(() => { + const now = new Date(); + const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const matchingTz = commonTimezones.find((tz) => tz.id === userTimezone); + if (matchingTz) { + setSourceTimezone(userTimezone); + } + setSourceTime(formatTimeForInput(now, userTimezone)); + setSourceDate(formatDateForInput(now, userTimezone)); + }, []); + + useEffect(() => { + if (sourceTime && sourceDate) { + const results = convertTime( + sourceTime, + sourceDate, + sourceTimezone, + selectedTimezones + ); + setConvertedTimes(results); + } + }, [sourceTime, sourceDate, sourceTimezone, selectedTimezones]); + + const handleTimezoneToggle = useCallback((timezoneId: string) => { + setSelectedTimezones((prev) => + prev.includes(timezoneId) + ? prev.filter((tz) => tz !== timezoneId) + : [...prev, timezoneId] + ); + }, []); + + const handleCopyAll = useCallback(() => { + const text = convertedTimes + .map((ct) => `${ct.label}: ${ct.time} (${ct.date})`) + .join("\n"); + handleCopy(text); + }, [convertedTimes, handleCopy]); + + const getSourceTimezoneLabel = () => { + const tz = commonTimezones.find((t) => t.id === sourceTimezone); + return tz?.label || sourceTimezone; + }; + + return ( +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+
+ + setSourceTime(e.target.value)} + /> +
+
+ + setSourceDate(e.target.value)} + /> +
+
+ +
+ +
+ {commonTimezones.map((tz) => ( +
+ handleTimezoneToggle(tz.id)} + /> + +
+ ))} +
+
+ + {convertedTimes.length > 0 && ( +
+ +
+ {convertedTimes.map((ct) => ( +
+ {ct.label} +
+ {ct.time} + + {ct.date} + +
+
+ ))} +
+
+ )} + + +
+
+ + + +
+ +
+
+ ); +} From db4ca87706d38abdcf0bc7de0d1f130e08fcc398 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:13:58 +0000 Subject: [PATCH 2/3] Fix Prettier formatting in TimezoneConverterSEO Co-Authored-By: dani@jam.dev --- components/seo/TimezoneConverterSEO.tsx | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/components/seo/TimezoneConverterSEO.tsx b/components/seo/TimezoneConverterSEO.tsx index 91e5753..1cdf586 100644 --- a/components/seo/TimezoneConverterSEO.tsx +++ b/components/seo/TimezoneConverterSEO.tsx @@ -6,9 +6,9 @@ export default function TimezoneConverterSEO() {

Easily convert times between different timezones with this free tool. - Perfect for remote software teams who need to schedule syncs, meetings, - and standups across multiple timezones. No more mental math or timezone - confusion. + Perfect for remote software teams who need to schedule syncs, + meetings, and standups across multiple timezones. No more mental math + or timezone confusion.

@@ -42,7 +42,8 @@ export default function TimezoneConverterSEO() { timezones.
  • - Step 3:
    Toggle timezones on/off to customize your view. + Step 3:
    Toggle timezones on/off to customize your + view.
  • @@ -51,21 +52,21 @@ export default function TimezoneConverterSEO() {

    Why Use a Timezone Converter?

    Remote teams often span multiple continents and timezones. Scheduling - meetings that work for everyone can be challenging. A timezone converter - helps you: + meetings that work for everyone can be challenging. A timezone + converter helps you:

    • - Schedule Meetings:
      Find times that work across different - regions without manual calculations. + Schedule Meetings:
      Find times that work across + different regions without manual calculations.
    • - Avoid Confusion:
      Eliminate timezone math errors that can - lead to missed meetings. + Avoid Confusion:
      Eliminate timezone math errors that + can lead to missed meetings.
    • - Plan Ahead:
      See how a proposed meeting time affects team - members in different locations. + Plan Ahead:
      See how a proposed meeting time affects + team members in different locations.
    From 74632a588c4d6cb3ec2f665a8a826b889bfe4a3c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:54:30 +0000 Subject: [PATCH 3/3] Add shareable URL query params for timezone converter - Read time, date, and tz params from URL on page load - Automatically update URL when user changes time/date/timezone - Add 'Copy Link to Share' button for easy sharing - URL format: /utilities/timezone-converter?time=15:30&date=2025-12-03&tz=America/Los_Angeles Co-Authored-By: dani@jam.dev --- pages/utilities/timezone-converter.tsx | 86 ++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 11 deletions(-) diff --git a/pages/utilities/timezone-converter.tsx b/pages/utilities/timezone-converter.tsx index 2251c00..c670c16 100644 --- a/pages/utilities/timezone-converter.tsx +++ b/pages/utilities/timezone-converter.tsx @@ -1,4 +1,5 @@ import { useCallback, useState, useEffect } from "react"; +import { useRouter } from "next/router"; import PageHeader from "@/components/PageHeader"; import { Card } from "@/components/ds/CardComponent"; import { Button } from "@/components/ds/ButtonComponent"; @@ -18,7 +19,14 @@ import { formatDateForInput, } from "@/components/utils/timezone-converter.utils"; +function getQueryParam( + value: string | string[] | undefined +): string | undefined { + return Array.isArray(value) ? value[0] : value; +} + export default function TimezoneConverter() { + const router = useRouter(); const [sourceTimezone, setSourceTimezone] = useState("America/New_York"); const [sourceTime, setSourceTime] = useState(""); const [sourceDate, setSourceDate] = useState(""); @@ -28,18 +36,61 @@ export default function TimezoneConverter() { const [convertedTimes, setConvertedTimes] = useState< { timezone: string; label: string; time: string; date: string }[] >([]); + const [isInitialized, setIsInitialized] = useState(false); const { buttonText, handleCopy } = useCopyToClipboard(); + const [linkButtonText, setLinkButtonText] = useState("Copy Link to Share"); useEffect(() => { - const now = new Date(); + if (!router.isReady) return; + + const timeParam = getQueryParam(router.query.time); + const dateParam = getQueryParam(router.query.date); + const tzParam = getQueryParam(router.query.tz); + const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const matchingTz = commonTimezones.find((tz) => tz.id === userTimezone); - if (matchingTz) { - setSourceTimezone(userTimezone); - } - setSourceTime(formatTimeForInput(now, userTimezone)); - setSourceDate(formatDateForInput(now, userTimezone)); - }, []); + const now = new Date(); + + const tzFromQuery = + tzParam && commonTimezones.some((t) => t.id === tzParam) + ? tzParam + : commonTimezones.some((t) => t.id === userTimezone) + ? userTimezone + : "America/New_York"; + + const timeFromQuery = + timeParam && /^\d{2}:\d{2}$/.test(timeParam) + ? timeParam + : formatTimeForInput(now, tzFromQuery); + + const dateFromQuery = + dateParam && /^\d{4}-\d{2}-\d{2}$/.test(dateParam) + ? dateParam + : formatDateForInput(now, tzFromQuery); + + setSourceTimezone(tzFromQuery); + setSourceTime(timeFromQuery); + setSourceDate(dateFromQuery); + setIsInitialized(true); + }, [router.isReady, router.query]); + + useEffect(() => { + if (!router.isReady || !isInitialized) return; + if (!sourceTime || !sourceDate || !sourceTimezone) return; + + router.replace( + { + pathname: router.pathname, + query: { + time: sourceTime, + date: sourceDate, + tz: sourceTimezone, + }, + }, + undefined, + { shallow: true } + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sourceTime, sourceDate, sourceTimezone, router.isReady, isInitialized]); useEffect(() => { if (sourceTime && sourceDate) { @@ -68,6 +119,14 @@ export default function TimezoneConverter() { handleCopy(text); }, [convertedTimes, handleCopy]); + const handleCopyLink = useCallback(() => { + if (typeof window === "undefined") return; + navigator.clipboard.writeText(window.location.href).then(() => { + setLinkButtonText("Copied!"); + setTimeout(() => setLinkButtonText("Copy Link to Share"), 2000); + }); + }, []); + const getSourceTimezoneLabel = () => { const tz = commonTimezones.find((t) => t.id === sourceTimezone); return tz?.label || sourceTimezone; @@ -170,9 +229,14 @@ export default function TimezoneConverter() { )} - +
    + + +