Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions src/islands/dev/CronExplainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { useState, useMemo } from 'react';
import { CalendarClock } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { CopyButton } from '@/components/ui/CopyButton';
import { Alert } from '@/components/ui/Alert';
import {
parseCron, explainCron, nextRuns,
FIELD_LABELS, CRON_PRESETS,
} from '@/tools/dev/cron.lib';
import type { Lang } from '@/i18n/config';

const TR: Record<Lang, {
exprLabel: string;
description: string;
nextRuns: string;
presets: string;
invalidExpr: string;
every: string;
noRuns: string;
fieldHints: string;
}> = {
en: {
exprLabel: 'Cron expression',
description: 'Description',
nextRuns: 'Next 10 run times',
presets: 'Presets',
invalidExpr: 'Invalid expression',
every: 'Every minute',
noRuns: 'No scheduled runs found in the next 4 years.',
fieldHints: 'Field hints',
},
id: {
exprLabel: 'Ekspresi cron',
description: 'Deskripsi',
nextRuns: '10 waktu eksekusi berikutnya',
presets: 'Preset',
invalidExpr: 'Ekspresi tidak valid',
every: 'Setiap menit',
noRuns: 'Tidak ada jadwal yang ditemukan dalam 4 tahun ke depan.',
fieldHints: 'Panduan field',
},
};

function formatDate(d: Date): string {
return d.toLocaleString(undefined, {
weekday: 'short', year: 'numeric', month: 'short',
day: '2-digit', hour: '2-digit', minute: '2-digit',
});
}

export default function CronExplainer({ lang = 'en' }: { lang?: Lang }) {
const t = TR[lang] ?? TR.en;
const [expr, setExpr] = useState('* * * * *');

const parsed = useMemo(() => parseCron(expr), [expr]);
const explanation = useMemo(() => {
if (!parsed.ok) return null;
return explainCron(parsed.cron);
}, [parsed]);
const runs = useMemo(() => {
if (!parsed.ok) return [];
return nextRuns(parsed.cron, new Date(), 10);
}, [parsed]);

const parts = expr.trim().split(/\s+/);
const fieldParts = parts.length === 5 ? parts : ['*', '*', '*', '*', '*'];

return (
<div className="space-y-4">
{/* Main input */}
<div className="space-y-2">
<label className="block text-sm font-bold uppercase tracking-wide text-muted-foreground">
{t.exprLabel}
</label>
<div className="flex items-center gap-2">
<input
value={expr}
onChange={e => setExpr(e.target.value)}
spellCheck={false}
className="flex-1 border-2 border-border bg-background px-3 py-2 font-mono text-xl outline-none focus:shadow-brutal-sm"
placeholder="* * * * *"
aria-label={t.exprLabel}
/>
<CopyButton value={expr} />
</div>

{/* Field labels */}
<div className="grid grid-cols-5 gap-1 pt-1">
{FIELD_LABELS.map((f, i) => (
<div key={f.key} className="text-center">
<div className={`border-2 py-1 px-1 font-mono text-sm font-bold ${
parsed.ok ? 'border-border bg-muted' : 'border-red-400 bg-red-50 dark:bg-red-950/30'
}`}>
{fieldParts[i] ?? '*'}
</div>
<div className="mt-1 text-xs text-muted-foreground leading-tight">{f.label}</div>
<div className="text-xs text-muted-foreground/60">{f.hint}</div>
</div>
))}
</div>
</div>

{/* Error */}
{!parsed.ok && (
<Alert variant="error">{t.invalidExpr}: {parsed.error}</Alert>
)}

{/* Description */}
{explanation && (
<div className="border-2 border-border bg-muted p-3">
<div className="mb-1 text-xs font-bold uppercase tracking-wide text-muted-foreground">{t.description}</div>
<p className="text-lg font-semibold">{explanation}</p>
</div>
)}

{/* Presets */}
<div className="space-y-2">
<div className="text-xs font-bold uppercase tracking-wide text-muted-foreground">{t.presets}</div>
<div className="flex flex-wrap gap-2">
{CRON_PRESETS.map(p => (
<Button
key={p.expr}
variant={expr === p.expr ? 'primary' : 'secondary'}
onClick={() => setExpr(p.expr)}
>
{p.label}
</Button>
))}
</div>
</div>

{/* Next runs */}
{parsed.ok && (
<div className="space-y-2">
<div className="text-xs font-bold uppercase tracking-wide text-muted-foreground flex items-center gap-2">
<CalendarClock className="h-3.5 w-3.5" />
{t.nextRuns}
</div>
{runs.length === 0 ? (
<p className="text-sm text-muted-foreground">{t.noRuns}</p>
) : (
<ol className="space-y-1">
{runs.map((d, i) => (
<li key={i} className="flex items-center gap-3 border-2 border-border bg-muted px-3 py-1.5">
<span className="w-6 shrink-0 text-right text-xs text-muted-foreground">{i + 1}</span>
<code className="flex-1 font-mono text-sm">{formatDate(d)}</code>
</li>
))}
</ol>
)}
</div>
)}
</div>
);
}
34 changes: 34 additions & 0 deletions src/registry/tool-seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ const en: Record<string, ToolSeoContent> = {
{ q: 'Are duplicates removed from the result?', a: 'Yes — every operation returns a de-duplicated list, preserving the first occurrence of each line and its original text.' },
],
},
'cron-expression': {
title: 'Free Cron Expression Editor — Cron Syntax Explainer Online',
description: 'Parse and explain cron expressions in plain English, see the next 10 scheduled run times, and pick from common presets. 100% client-side; nothing is uploaded.',
intro: 'This free cron expression editor explains any standard cron schedule in plain English and shows the next 10 run times — so you can verify your schedule at a glance. Type or paste a 5-field cron expression (minute hour dom month weekday), pick a preset, and see exactly when your job will fire next. Everything runs in your browser; nothing is sent to a server.',
howTo: [
'Type a 5-field cron expression in the box (e.g. 0 9 * * 1-5 for weekdays at 09:00).',
'The human-readable description updates instantly as you type.',
'Check the "Next 10 run times" list to confirm the schedule looks right.',
'Use the preset buttons for common schedules like every 15 minutes, daily, or monthly.',
],
faqs: [
{ q: 'What is the cron field order?', a: 'Standard unix cron uses 5 fields left-to-right: minute (0–59), hour (0–23), day of month (1–31), month (1–12), and day of week (0–7, where both 0 and 7 mean Sunday).' },
{ q: 'What special characters can I use?', a: 'Star (*) matches every value; */n means every n units; a-b is a range; a,b,c is a list; and a-b/n is a range with step. For example */15 fires every 15 minutes and 1-5 means Monday through Friday.' },
{ q: 'How does the DOM/DOW interaction work?', a: 'Standard unix cron: if both the day-of-month and day-of-week fields are restricted (not *), a time fires when EITHER condition matches — the two are combined with OR, not AND.' },
{ q: 'Is my cron expression sent to a server?', a: 'No. Parsing, explaining and computing run times all happen entirely in your browser with JavaScript. Nothing is ever uploaded.' },
],
},
'docx-to-pdf': {
title: 'Free DOCX to PDF Converter — Word to PDF Online',
description: 'Convert Word (.docx) documents to PDF right in your browser — page-accurate, with your layout, tables and images. 100% private; nothing is uploaded.',
Expand Down Expand Up @@ -1495,6 +1512,23 @@ const id: Record<string, ToolSeoContent> = {
{ q: 'Apakah duplikat dihapus dari hasil?', a: 'Ya — setiap operasi mengembalikan daftar tanpa duplikat, mempertahankan kemunculan pertama setiap baris beserta teks aslinya.' },
],
},
'cron-expression': {
title: 'Tool Ekspresi Cron Gratis — Penjelas Sintaks Cron Online',
description: 'Urai dan jelaskan ekspresi cron dalam bahasa Indonesia yang mudah dipahami, lihat 10 waktu eksekusi berikutnya, dan pilih dari preset umum. 100% di sisi klien; tidak ada yang diunggah.',
intro: 'Tool ekspresi cron gratis ini menjelaskan jadwal cron dalam bahasa yang mudah dipahami dan menampilkan 10 waktu eksekusi berikutnya — sehingga Anda bisa memverifikasi jadwal sekilas. Ketik atau tempel ekspresi cron 5-field (menit jam hari-bulan bulan hari-minggu), pilih preset, dan lihat kapan pekerjaan Anda akan berjalan selanjutnya. Semuanya berjalan di browser; tidak ada yang dikirim ke server.',
howTo: [
'Ketik ekspresi cron 5-field di kotak (misalnya 0 9 * * 1-5 untuk hari kerja pukul 09:00).',
'Deskripsi dalam bahasa yang mudah dipahami diperbarui secara instan saat Anda mengetik.',
'Periksa daftar "10 waktu eksekusi berikutnya" untuk memastikan jadwal sudah benar.',
'Gunakan tombol preset untuk jadwal umum seperti setiap 15 menit, harian, atau bulanan.',
],
faqs: [
{ q: 'Apa urutan field dalam cron?', a: 'Cron unix standar menggunakan 5 field dari kiri ke kanan: menit (0–59), jam (0–23), hari-dalam-bulan (1–31), bulan (1–12), dan hari-dalam-minggu (0–7, di mana 0 dan 7 keduanya berarti Minggu).' },
{ q: 'Karakter khusus apa yang bisa saya gunakan?', a: 'Tanda bintang (*) cocok dengan setiap nilai; */n berarti setiap n unit; a-b adalah rentang; a,b,c adalah daftar; dan a-b/n adalah rentang dengan langkah. Misalnya */15 berjalan setiap 15 menit dan 1-5 berarti Senin hingga Jumat.' },
{ q: 'Bagaimana interaksi DOM/DOW bekerja?', a: 'Cron unix standar: jika field hari-dalam-bulan dan hari-dalam-minggu keduanya dibatasi (bukan *), jadwal berjalan ketika SALAH SATU kondisi terpenuhi — keduanya digabung dengan OR, bukan AND.' },
{ q: 'Apakah ekspresi cron saya dikirim ke server?', a: 'Tidak. Penguraian, penjelasan, dan penghitungan waktu eksekusi semuanya terjadi sepenuhnya di browser Anda dengan JavaScript. Tidak ada yang pernah diunggah.' },
],
},
'docx-to-pdf': {
title: 'Konverter DOCX ke PDF Gratis — Word ke PDF Online',
description: 'Konversi dokumen Word (.docx) ke PDF langsung di browser Anda — akurat per halaman, dengan tata letak, tabel, dan gambar. 100% privat; tidak ada yang diunggah.',
Expand Down
13 changes: 12 additions & 1 deletion src/registry/tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Hash, Braces, Binary, Link, KeyRound, Fingerprint, KeySquare, FileDiff, Table, FileText, QrCode, ScanLine, Clock, Calculator, Palette, FilePlus2, Scissors, RotateCw, FileImage, FileX, Stamp, Image, Replace, Minimize2, Maximize2, Eraser, Archive, Lock, Unlock, Crop, Droplet, PenTool, Combine, ShieldCheck, FileCode, FileCode2, FileCog, FileArchive, FolderArchive, Sparkles, ScanFace, Scaling, Aperture, Wand2, PenLine, Shapes, Film, FileVideo, Music, AudioLines, MonitorPlay, Camera, Code2, Database, Keyboard, Contrast, Eye, ScanText, Receipt, Webcam, Mic, Send, Video, Wrench, Compass, Map, Waypoints, ImageDown, ScrollText, Ghost, FileSpreadsheet, BookOpen, FileType2, FileDown, GitCompare, FileOutput } from 'lucide-react';
import { Hash, Braces, Binary, Link, KeyRound, Fingerprint, KeySquare, FileDiff, Table, FileText, QrCode, ScanLine, Clock, Calculator, Palette, FilePlus2, Scissors, RotateCw, FileImage, FileX, Stamp, Image, Replace, Minimize2, Maximize2, Eraser, Archive, Lock, Unlock, Crop, Droplet, PenTool, Combine, ShieldCheck, FileCode, FileCode2, FileCog, FileArchive, FolderArchive, Sparkles, ScanFace, Scaling, Aperture, Wand2, PenLine, Shapes, Film, FileVideo, Music, AudioLines, MonitorPlay, Camera, Code2, Database, Keyboard, Contrast, Eye, ScanText, Receipt, Webcam, Mic, Send, Video, Wrench, Compass, Map, Waypoints, ImageDown, ScrollText, Ghost, FileSpreadsheet, BookOpen, FileType2, FileDown, GitCompare, FileOutput, CalendarClock } from 'lucide-react';
import type { ToolDef } from '@/types/tool';

export const tools: ToolDef[] = [
Expand Down Expand Up @@ -179,6 +179,17 @@ export const tools: ToolDef[] = [
load: () => import('@/islands/dev/SqlFormat'),
status: 'beta'
},
{
id: 'cron-expression',
name: 'Cron Expression',
category: 'Dev',
route: '/tools/cron-expression',
keywords: ['cron', 'crontab', 'schedule', 'scheduler', 'expression', 'timer', 'interval', 'recurring', 'task', 'job', 'unix', 'linux', 'automation'],
icon: CalendarClock,
summary: 'Parse and explain cron expressions with next scheduled run times',
load: () => import('@/islands/dev/CronExplainer'),
status: 'beta'
},
{
id: 'docx-viewer',
name: 'Word (DOCX) Viewer',
Expand Down
Loading
Loading