1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react@18.2.0 */
import cronstrue from "https://esm.sh/cronstrue";
import React, { useState } from "https://esm.sh/react@18.2.0";
import { chat } from "https://esm.town/v/stevekrouse/openai?v=19";
import react_http from "https://esm.town/v/stevekrouse/react_http?v=6";
export default function(req: Request) {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname === "/") {
return react_http({
component: App,
sourceURL: import.meta.url,
head: `<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<title>CronGPT</title>`,
});
} else if (req.method === "POST" && url.pathname === "/compile") {
return compile(req);
}
return new Response("Not found", { status: 404 });
}
export function App() {
const browserTZ = Intl.DateTimeFormat().resolvedOptions().timeZone;
const [loading, setLoading] = useState(false);
const [cron, setCron] = useState("0 16 * * 1-5");
const onSubmit: React.FormEventHandler<HTMLFormElement> = async function(e) {
e.preventDefault();
setLoading(true);
const cron = await fetch("/compile", {
method: "POST",
body: new FormData(e.target as HTMLFormElement),
}).then((r) => r.json());
setCron(cron);
setLoading(false);
};
return (
<div className="flex p-6 mt-4 flex-col space-y-12 max-w-2xl mx-auto">
<div className="flex flex-col text-center space-y-2">
<h1 className="text-3xl font-bold">CronGPT</h1>
<p className="text-lg">Generate cron expressions with ChatGPT</p>
</div>
<form className="flex flex-col space-y-4" onSubmit={onSubmit}>
<div className="flex flex-col">
<label>Natural language description</label>
<input
name="description"
defaultValue="On weekdays at noon"
required
className="border-2 rounded-lg p-2 text-lg text-center"
/>
</div>
<div className="flex flex-col">
<label>Timezone</label>
<select name="timezone" className=" border-2 rounded-lg text-lg p-2 text-center">
{Intl.supportedValuesOf("timeZone").map((tz) => <option value={tz} selected={tz === browserTZ}>{tz}
</option>)}
</select>
</div>
<button
className="bg-sky-500 hover:bg-sky-700 text-white font-bold py-2 px-4 rounded disabled:bg-gray-500"
disabled={loading}
>
{loading ? "Loading.." : "Compile"}
</button>
</form>
<Cron cron={cron} timezone="America/New_York" />
<div className="text-gray-500 text-center flex flex-col space-y-2">
<div>
Need a place to run cron jobs? Try <Link href="https://val.town">Val Town</Link>
</div>{" "}
<div>
<Link href="https://www.val.town/v/stevekrouse/cron">View source</Link>
{" | "} Inspired by <Link href="https://cronprompt.com/">Cron Prompt</Link>
</div>
</div>
</div>
);
}
const Link = ({ href, children }: { href: string; children?: any }) => (
<a href={href} className="text-blue-500 hover:text-blue-700" target="_blank" rel="noopener noreferrer">{children}</a>
);
function Cron({ cron, timezone }) {
let translation;
try {
translation = cronstrue.toString(cron, { tzOffset: getOffset(timezone), use24HourTimeFormat: false });
} catch (e) {
translation = "Translation error: " + e.message;
}
return (
<div id="cron" className="flex flex-col space-y-4">
<pre className="font-mono text-2xl flex flex-row justify-between w-full border-2 p-2 bg-gray-200">
<div></div>
<div className="flex">
<div className="font-bold select-all">{cron}</div>
</div>
<div className="text-gray-400 font-bold mr-2 select-none">UTC</div>