Skip to content

Commit 102e6c7

Browse files
committed
feat: Timezone tool addition
1 parent 5ca408a commit 102e6c7

File tree

17 files changed

+1017
-16
lines changed

17 files changed

+1017
-16
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Devbox is a lightweight, cross-platform desktop application built with Tauri (Ru
5555
- [x] DNS Lookup Tool
5656
- [x] Certificate Decoder X.509
5757
- [x] JSON Formatter
58+
- [x] Timezone
5859
- [ ] Diff tools
5960
- [ ] CSS Playground
6061
- [ ] Color Testing
@@ -74,6 +75,5 @@ Devbox is a lightweight, cross-platform desktop application built with Tauri (Ru
7475
- [ ] JSON <> YAML
7576
- [ ] Hashing Text
7677
- [ ] Hasing Files
77-
- [ ] Timezone
7878
- [ ] WebSocket Client
7979
- [ ] Mock API Server / Webhook test

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
"chart.js": "^4.5.0",
4444
"chartjs-chart-treemap": "^3.1.0",
4545
"chroma-js": "^2.4.2",
46+
"city-timezones": "^1.3.1",
4647
"clsx": "^2.1.0",
48+
"countries-and-timezones": "^3.6.0",
4749
"cron-parser": "^4.9.0",
4850
"cronstrue": "^2.48.0",
4951
"date-fns": "^3.6.0",

src/App.module.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,20 @@
1818
width: 100%;
1919
overflow: hidden;
2020
}
21+
22+
.buttonRoot {
23+
&[data-variant='danger'] {
24+
background-color: var(--mantine-color-red-8);
25+
color: var(--mantine-color-white);
26+
&:hover {
27+
background-color: var(--mantine-color-red-9);
28+
}
29+
}
30+
/* Override default disabled styles */
31+
&[data-variant='danger']:disabled {
32+
background-color: transparent;
33+
color: var(--mantine-color-white);
34+
opacity: 0.5;
35+
cursor: not-allowed;
36+
}
37+
}

src/Components/AppRoutes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const componentMap = {
2727
"url-encoder": loadable(() => import("@/Features/url/UrlEncoder")) as React.ComponentType,
2828
"certificate-decoder": loadable(() => import("../Features/x509/X509")) as React.ComponentType,
2929
"json-formatter": loadable(() => import("../Features/json/JsonFormatter")) as React.ComponentType,
30+
timezone: loadable(() => import("@/Features/timezone/Timezone")) as React.ComponentType,
3031
};
3132
// Dynamically create lazy-loaded components
3233
const routes = tools

src/Features/cron/utils/cronHelpers.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import parser from "cron-parser";
22
import cronstrue from "cronstrue";
3-
import { formatDistance, format } from "date-fns";
4-
import { CronValidation, CronExecution } from "../types/cron.types";
3+
import { format, formatDistance } from "date-fns";
4+
import { CronExecution, CronValidation } from "../types/cron.types";
55

66
export const validateCronExpression = (expression: string): CronValidation => {
77
try {
@@ -31,10 +31,6 @@ export const getNextExecutions = (expression: string, count: number = 3): CronEx
3131

3232
for (let i = 0; i < count; i++) {
3333
const nextDate = interval.next().toDate();
34-
console.log(
35-
"Next execution date:",
36-
formatDistance(new Date(), nextDate, { addSuffix: true })
37-
);
3834
executions.push({
3935
date: nextDate,
4036
humanReadable: format(nextDate, "yyyy-MM-dd HH:mm:ss"),

src/Features/ssh/hooks/useKeyGeneration.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useState, useCallback } from "react";
2-
import { KeyGenerationOptions, GeneratedKeyPair } from "../types/ssh";
1+
import { useCallback, useState } from "react";
2+
import { GeneratedKeyPair, KeyGenerationOptions } from "../types/ssh";
33
import { generateKeyPair } from "../utils/crypto";
44

55
export function useKeyGeneration() {
@@ -12,12 +12,9 @@ export function useKeyGeneration() {
1212
setError(null);
1313

1414
try {
15-
console.log("Generating keys with options:", options);
1615
const keyPair = await generateKeyPair(options);
17-
console.log("Key generation successful");
1816
setGeneratedKeys(keyPair);
1917
} catch (err) {
20-
console.error("Key generation failed:", err);
2118
setError(err instanceof Error ? err.message : "Failed to generate key pair");
2219
} finally {
2320
setIsGenerating(false);

src/Features/timezone/Timezone.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { settingsStore } from "@/utils/store";
2+
import { Button, Group, SegmentedControl, Stack } from "@mantine/core";
3+
import { useEffect, useState } from "react";
4+
import TimezonesBoard from "./components/TimezonesBoard";
5+
import { TimeFormat, TIMEZONE_PREFERENCES_KEY, TimezonePreferencesV1, TimezoneRow } from "./types";
6+
7+
const DEFAULT_PREFS: TimezonePreferencesV1 = {
8+
version: 1,
9+
rows: [{ id: crypto.randomUUID(), label: "India", timeZone: "Asia/Kolkata" }],
10+
timeFormat: "h24",
11+
lastReferenceIso: undefined,
12+
sliderZoom: "hours",
13+
};
14+
15+
export default function Timezone() {
16+
const [preferences, setPreferences] = useState<TimezonePreferencesV1>(DEFAULT_PREFS);
17+
const [reference, setReference] = useState<Date>(() =>
18+
preferences.lastReferenceIso ? new Date(preferences.lastReferenceIso) : new Date()
19+
);
20+
const [live, setLive] = useState<boolean>(true);
21+
22+
// Keep reference ticking in live mode
23+
useEffect(() => {
24+
if (!live) return;
25+
const interval = 30000;
26+
const id = window.setInterval(() => setReference(new Date()), interval);
27+
return () => window.clearInterval(id);
28+
}, [live]);
29+
useEffect(() => {
30+
let mounted = true;
31+
(async () => {
32+
const saved = await settingsStore.get<TimezonePreferencesV1>(TIMEZONE_PREFERENCES_KEY);
33+
if (mounted && saved && saved.version === 1) {
34+
setPreferences(saved);
35+
setReference(saved.lastReferenceIso ? new Date(saved.lastReferenceIso) : new Date());
36+
}
37+
})();
38+
return () => {
39+
mounted = false;
40+
};
41+
}, []);
42+
43+
useEffect(() => {
44+
const toSave: TimezonePreferencesV1 = {
45+
...preferences,
46+
lastReferenceIso: reference.toISOString(),
47+
};
48+
settingsStore.update(TIMEZONE_PREFERENCES_KEY, toSave);
49+
}, [preferences, reference]);
50+
51+
const handleAdd = () => {
52+
// Add a draft row that will render inline editor
53+
const draft: TimezoneRow = {
54+
id: crypto.randomUUID(),
55+
label: "",
56+
timeZone: "UTC",
57+
isDraft: true,
58+
isNew: true,
59+
};
60+
setPreferences(prev => ({ ...prev, rows: [...prev.rows, draft] }));
61+
};
62+
63+
const handleRemove = (id: string) => {
64+
setPreferences(prev => ({ ...prev, rows: prev.rows.filter(r => r.id !== id) }));
65+
};
66+
67+
return (
68+
<Stack className="overflow-padding overflow-auto" h="100%" gap="md" pt="xl">
69+
<Group justify="space-between" wrap="nowrap">
70+
<Group>
71+
<SegmentedControl
72+
data={[
73+
{ value: "h12", label: "12h" },
74+
{ value: "h24", label: "24h" },
75+
]}
76+
value={preferences.timeFormat}
77+
onChange={e => {
78+
setPreferences(prev => ({
79+
...prev,
80+
timeFormat: e as TimeFormat,
81+
}));
82+
}}
83+
size="xs"
84+
/>
85+
<Button
86+
size="xs"
87+
variant="danger"
88+
disabled={live}
89+
onClick={() => {
90+
setLive(true);
91+
setReference(new Date());
92+
}}
93+
>
94+
Reset to now
95+
</Button>
96+
</Group>
97+
<Button size="xs" variant="light" onClick={handleAdd}>
98+
Add timezone
99+
</Button>
100+
</Group>
101+
102+
<TimezonesBoard
103+
rows={preferences.rows}
104+
reference={reference}
105+
timeFormat={preferences.timeFormat}
106+
onRemove={handleRemove}
107+
onPauseLive={() => setLive(false)}
108+
onSetReference={setReference}
109+
isLive={live}
110+
onRowChange={(row: TimezoneRow) =>
111+
setPreferences(prev => ({
112+
...prev,
113+
rows: prev.rows.map(r => (r.id === row.id ? row : r)),
114+
}))
115+
}
116+
/>
117+
</Stack>
118+
);
119+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Group, SegmentedControl, Slider, Text } from "@mantine/core";
2+
import { useMemo } from "react";
3+
import { SliderZoom } from "../types";
4+
5+
interface Props {
6+
reference: Date;
7+
onChange: (date: Date) => void;
8+
zoom: SliderZoom;
9+
onZoomChange: (zoom: SliderZoom) => void;
10+
}
11+
12+
export default function TimeSlider({ reference, onChange, zoom, onZoomChange }: Props) {
13+
const range = useMemo(() => {
14+
if (zoom === "hours") {
15+
return { min: -12, max: 12, step: 0.25, unitMs: 3600000 } as const; // 15 min
16+
}
17+
return { min: -14, max: 14, step: 1, unitMs: 86400000 } as const; // 1 day
18+
}, [zoom]);
19+
20+
const value = 0; // centered at reference
21+
22+
return (
23+
<Group align="center" justify="space-between">
24+
<SegmentedControl
25+
value={zoom}
26+
onChange={v => onZoomChange(v as SliderZoom)}
27+
data={[
28+
{ label: "Hours", value: "hours" },
29+
{ label: "Days", value: "days" },
30+
]}
31+
/>
32+
<Slider
33+
min={range.min}
34+
max={range.max}
35+
step={range.step}
36+
value={value}
37+
onChange={delta => {
38+
const ms = delta * range.unitMs;
39+
onChange(new Date(reference.getTime() + ms));
40+
}}
41+
style={{ flex: 1 }}
42+
label={val => `${val} ${zoom === "hours" ? "h" : "d"}`}
43+
/>
44+
<Text size="sm" c="dimmed">
45+
{reference.toISOString()}
46+
</Text>
47+
</Group>
48+
);
49+
}

0 commit comments

Comments
 (0)