huge translations work

This commit is contained in:
fccview
2025-11-05 21:41:15 +00:00
parent b1a4d081ad
commit b9fb009923
26 changed files with 1124 additions and 537 deletions

View File

@@ -2,42 +2,16 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/GlobalComponents/Cards/Card";
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
import {
Trash2,
Clock,
Edit,
Plus,
Files,
User,
Play,
Pause,
Code,
} from "lucide-react";
import { Clock, Plus } from "lucide-react";
import { CronJob } from "@/app/_utils/cronjob-utils";
import { useState, useMemo, useEffect } from "react";
import { CreateTaskModal } from "@/app/_components/FeatureComponents/Modals/CreateTaskModal";
import { EditTaskModal } from "@/app/_components/FeatureComponents/Modals/EditTaskModal";
import { DeleteTaskModal } from "@/app/_components/FeatureComponents/Modals/DeleteTaskModal";
import { CloneTaskModal } from "@/app/_components/FeatureComponents/Modals/CloneTaskModal";
import { UserFilter } from "@/app/_components/FeatureComponents/User/UserFilter";
import { ErrorBadge } from "@/app/_components/GlobalComponents/Badges/ErrorBadge";
import { ErrorDetailsModal } from "@/app/_components/FeatureComponents/Modals/ErrorDetailsModal";
import { Script } from "@/app/_utils/scripts-utils";
import { UserFilter } from "@/app/_components/FeatureComponents/User/UserFilter";
import {
getJobErrorsByJobId,
JobError,
} from "@/app/_utils/error-utils";
import {
handleErrorClick,
handleDelete,
handleClone,
handlePause,
handleResume,
handleRun,
handleEditSubmit,
handleNewCronSubmit,
} from "@/app/_components/FeatureComponents/Cronjobs/helpers";
import { useCronJobState } from "@/app/_hooks/useCronJobState";
import { CronJobItem } from "@/app/_components/FeatureComponents/Cronjobs/Parts/CronJobItem";
import { CronJobEmptyState } from "@/app/_components/FeatureComponents/Cronjobs/Parts/CronJobEmptyState";
import { CronJobListModals } from "@/app/_components/FeatureComponents/Modals/CronJobListsModals";
import { useTranslations } from "next-intl";
interface CronJobListProps {
cronJobs: CronJob[];
@@ -45,206 +19,46 @@ interface CronJobListProps {
}
export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
const [deletingId, setDeletingId] = useState<string | null>(null);
const [editingJob, setEditingJob] = useState<CronJob | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isNewCronModalOpen, setIsNewCronModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isCloneModalOpen, setIsCloneModalOpen] = useState(false);
const [jobToDelete, setJobToDelete] = useState<CronJob | null>(null);
const [jobToClone, setJobToClone] = useState<CronJob | null>(null);
const [isCloning, setIsCloning] = useState(false);
const [runningJobId, setRunningJobId] = useState<string | null>(null);
const [selectedUser, setSelectedUser] = useState<string | null>(null);
const [jobErrors, setJobErrors] = useState<Record<string, JobError[]>>({});
const [errorModalOpen, setErrorModalOpen] = useState(false);
const [selectedError, setSelectedError] = useState<JobError | null>(null);
useEffect(() => {
const savedUser = localStorage.getItem("selectedCronUser");
if (savedUser) {
setSelectedUser(savedUser);
}
}, []);
useEffect(() => {
if (selectedUser) {
localStorage.setItem("selectedCronUser", selectedUser);
} else {
localStorage.removeItem("selectedCronUser");
}
}, [selectedUser]);
const [editForm, setEditForm] = useState({
schedule: "",
command: "",
comment: "",
});
const [newCronForm, setNewCronForm] = useState({
schedule: "",
command: "",
comment: "",
selectedScriptId: null as string | null,
user: "",
});
const filteredJobs = useMemo(() => {
if (!selectedUser) return cronJobs;
return cronJobs.filter((job) => job.user === selectedUser);
}, [cronJobs, selectedUser]);
useEffect(() => {
const errors: Record<string, JobError[]> = {};
filteredJobs.forEach((job) => {
errors[job.id] = getJobErrorsByJobId(job.id);
});
setJobErrors(errors);
}, [filteredJobs]);
const handleErrorClickLocal = (error: JobError) => {
handleErrorClick(error, setSelectedError, setErrorModalOpen);
};
const refreshJobErrorsLocal = () => {
const errors: Record<string, JobError[]> = {};
filteredJobs.forEach((job) => {
errors[job.id] = getJobErrorsByJobId(job.id);
});
setJobErrors(errors);
};
const handleDeleteLocal = async (id: string) => {
await handleDelete(id, {
setDeletingId,
setIsDeleteModalOpen,
setJobToDelete,
setIsCloneModalOpen,
setJobToClone,
setIsCloning,
setIsEditModalOpen,
setEditingJob,
setIsNewCronModalOpen,
setNewCronForm,
setRunningJobId,
refreshJobErrors: refreshJobErrorsLocal,
jobToClone,
editingJob,
editForm,
newCronForm,
});
};
const handleCloneLocal = async (newComment: string) => {
await handleClone(newComment, {
setDeletingId,
setIsDeleteModalOpen,
setJobToDelete,
setIsCloneModalOpen,
setJobToClone,
setIsCloning,
setIsEditModalOpen,
setEditingJob,
setIsNewCronModalOpen,
setNewCronForm,
setRunningJobId,
refreshJobErrors: refreshJobErrorsLocal,
jobToClone,
editingJob,
editForm,
newCronForm,
});
};
const handlePauseLocal = async (id: string) => {
await handlePause(id);
};
const handleResumeLocal = async (id: string) => {
await handleResume(id);
};
const handleRunLocal = async (id: string) => {
await handleRun(id, {
setDeletingId,
setIsDeleteModalOpen,
setJobToDelete,
setIsCloneModalOpen,
setJobToClone,
setIsCloning,
setIsEditModalOpen,
setEditingJob,
setIsNewCronModalOpen,
setNewCronForm,
setRunningJobId,
refreshJobErrors: refreshJobErrorsLocal,
jobToClone,
editingJob,
editForm,
newCronForm,
});
};
const confirmDelete = (job: CronJob) => {
setJobToDelete(job);
setIsDeleteModalOpen(true);
};
const confirmClone = (job: CronJob) => {
setJobToClone(job);
setIsCloneModalOpen(true);
};
const handleEdit = (job: CronJob) => {
setEditingJob(job);
setEditForm({
schedule: job.schedule,
command: job.command,
comment: job.comment || "",
});
setIsEditModalOpen(true);
};
const handleEditSubmitLocal = async (e: React.FormEvent) => {
await handleEditSubmit(e, {
setDeletingId,
setIsDeleteModalOpen,
setJobToDelete,
setIsCloneModalOpen,
setJobToClone,
setIsCloning,
setIsEditModalOpen,
setEditingJob,
setIsNewCronModalOpen,
setNewCronForm,
setRunningJobId,
refreshJobErrors: refreshJobErrorsLocal,
jobToClone,
editingJob,
editForm,
newCronForm,
});
};
const handleNewCronSubmitLocal = async (e: React.FormEvent) => {
await handleNewCronSubmit(e, {
setDeletingId,
setIsDeleteModalOpen,
setJobToDelete,
setIsCloneModalOpen,
setJobToClone,
setIsCloning,
setIsEditModalOpen,
setEditingJob,
setIsNewCronModalOpen,
setNewCronForm,
setRunningJobId,
refreshJobErrors: refreshJobErrorsLocal,
jobToClone,
editingJob,
editForm,
newCronForm,
});
};
const t = useTranslations();
const {
deletingId,
runningJobId,
selectedUser,
setSelectedUser,
jobErrors,
errorModalOpen,
setErrorModalOpen,
selectedError,
setSelectedError,
filteredJobs,
isNewCronModalOpen,
setIsNewCronModalOpen,
isEditModalOpen,
setIsEditModalOpen,
isDeleteModalOpen,
setIsDeleteModalOpen,
isCloneModalOpen,
setIsCloneModalOpen,
jobToDelete,
jobToClone,
isCloning,
editForm,
setEditForm,
newCronForm,
setNewCronForm,
handleErrorClickLocal,
refreshJobErrorsLocal,
handleDeleteLocal,
handleCloneLocal,
handlePauseLocal,
handleResumeLocal,
handleRunLocal,
confirmDelete,
confirmClone,
handleEdit,
handleEditSubmitLocal,
handleNewCronSubmitLocal,
} = useCronJobState({ cronJobs, scripts });
return (
<>
@@ -257,12 +71,12 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
</div>
<div>
<CardTitle className="text-xl brand-gradient">
Scheduled Tasks
{t("cronjobs.scheduledTasks")}
</CardTitle>
<p className="text-sm text-muted-foreground">
{filteredJobs.length} of {cronJobs.length} scheduled job
{filteredJobs.length !== 1 ? "s" : ""}
{selectedUser && ` for ${selectedUser}`}
{t("cronjobs.nOfNJObs", { filtered: filteredJobs.length, total: cronJobs.length })}
{" "}
{selectedUser && t("cronjobs.forUser", { user: selectedUser })}
</p>
</div>
</div>
@@ -271,7 +85,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
className="btn-primary glow-primary"
>
<Plus className="h-4 w-4 mr-2" />
New Task
{t("cronjobs.newTask")}
</Button>
</div>
</CardHeader>
@@ -285,219 +99,74 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
</div>
{filteredJobs.length === 0 ? (
<div className="text-center py-16">
<div className="mx-auto w-20 h-20 bg-gradient-to-br from-primary/20 to-blue-500/20 rounded-full flex items-center justify-center mb-6">
<Clock className="h-10 w-10 text-primary" />
</div>
<h3 className="text-xl font-semibold mb-3 brand-gradient">
{selectedUser
? `No tasks for user ${selectedUser}`
: "No scheduled tasks yet"}
</h3>
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
{selectedUser
? `No scheduled tasks found for user ${selectedUser}. Try selecting a different user or create a new task.`
: "Create your first scheduled task to automate your system operations and boost productivity."}
</p>
<Button
onClick={() => setIsNewCronModalOpen(true)}
className="btn-primary glow-primary"
size="lg"
>
<Plus className="h-5 w-5 mr-2" />
Create Your First Task
</Button>
</div>
<CronJobEmptyState
selectedUser={selectedUser}
onNewTaskClick={() => setIsNewCronModalOpen(true)}
/>
) : (
<div className="space-y-3">
{filteredJobs.map((job) => (
<div
<CronJobItem
key={job.id}
className="glass-card p-4 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors"
>
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
<div className="flex-1 min-w-0 order-2 lg:order-1">
<div className="flex items-center gap-3 mb-2">
<code className="text-sm bg-purple-500/10 text-purple-600 dark:text-purple-400 px-2 py-1 rounded font-mono border border-purple-500/20">
{job.schedule}
</code>
<div className="flex-1 min-w-0">
<pre
className="text-sm font-medium text-foreground truncate bg-muted/30 px-2 py-1 rounded border border-border/30"
title={job.command}
>
{job.command}
</pre>
</div>
</div>
<div className="flex items-center gap-2 mb-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<User className="h-3 w-3" />
<span>{job.user}</span>
</div>
{job.paused && (
<span className="text-xs bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 px-2 py-0.5 rounded border border-yellow-500/20">
Paused
</span>
)}
<ErrorBadge
errors={jobErrors[job.id] || []}
onErrorClick={handleErrorClickLocal}
onErrorDismiss={refreshJobErrorsLocal}
/>
</div>
{job.comment && (
<p
className="text-xs text-muted-foreground italic truncate"
title={job.comment}
>
{job.comment}
</p>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0 order-1 lg:order-2">
<Button
variant="outline"
size="sm"
onClick={() => handleRunLocal(job.id)}
disabled={runningJobId === job.id || job.paused}
className="btn-outline h-8 px-3"
title="Run cron job manually"
aria-label="Run cron job manually"
>
{runningJobId === job.id ? (
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
<Code className="h-3 w-3" />
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(job)}
className="btn-outline h-8 px-3"
title="Edit cron job"
aria-label="Edit cron job"
>
<Edit className="h-3 w-3" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => confirmClone(job)}
className="btn-outline h-8 px-3"
title="Clone cron job"
aria-label="Clone cron job"
>
<Files className="h-3 w-3" />
</Button>
{job.paused ? (
<Button
variant="outline"
size="sm"
onClick={() => handleResumeLocal(job.id)}
className="btn-outline h-8 px-3"
title="Resume cron job"
aria-label="Resume cron job"
>
<Play className="h-3 w-3" />
</Button>
) : (
<Button
variant="outline"
size="sm"
onClick={() => handlePauseLocal(job.id)}
className="btn-outline h-8 px-3"
title="Pause cron job"
aria-label="Pause cron job"
>
<Pause className="h-3 w-3" />
</Button>
)}
<Button
variant="destructive"
size="sm"
onClick={() => confirmDelete(job)}
disabled={deletingId === job.id}
className="btn-destructive h-8 px-3"
title="Delete cron job"
aria-label="Delete cron job"
>
{deletingId === job.id ? (
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
<Trash2 className="h-3 w-3" />
)}
</Button>
</div>
</div>
</div>
job={job}
errors={jobErrors[job.id] || []}
runningJobId={runningJobId}
deletingId={deletingId}
onRun={handleRunLocal}
onEdit={handleEdit}
onClone={confirmClone}
onResume={handleResumeLocal}
onPause={handlePauseLocal}
onDelete={confirmDelete}
onErrorClick={handleErrorClickLocal}
onErrorDismiss={refreshJobErrorsLocal}
/>
))}
</div>
)}
</CardContent>
</Card>
<CreateTaskModal
isOpen={isNewCronModalOpen}
onClose={() => setIsNewCronModalOpen(false)}
onSubmit={handleNewCronSubmitLocal}
<CronJobListModals
cronJobs={cronJobs}
scripts={scripts}
form={newCronForm}
onFormChange={(updates) =>
isNewCronModalOpen={isNewCronModalOpen}
onNewCronModalClose={() => setIsNewCronModalOpen(false)}
onNewCronSubmit={handleNewCronSubmitLocal}
newCronForm={newCronForm}
onNewCronFormChange={(updates) =>
setNewCronForm((prev) => ({ ...prev, ...updates }))
}
/>
<EditTaskModal
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
onSubmit={handleEditSubmitLocal}
form={editForm}
onFormChange={(updates) =>
isEditModalOpen={isEditModalOpen}
onEditModalClose={() => setIsEditModalOpen(false)}
onEditSubmit={handleEditSubmitLocal}
editForm={editForm}
onEditFormChange={(updates) =>
setEditForm((prev) => ({ ...prev, ...updates }))
}
/>
<DeleteTaskModal
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
onConfirm={() =>
isDeleteModalOpen={isDeleteModalOpen}
onDeleteModalClose={() => setIsDeleteModalOpen(false)}
onDeleteConfirm={() =>
jobToDelete ? handleDeleteLocal(jobToDelete.id) : undefined
}
job={jobToDelete}
/>
jobToDelete={jobToDelete}
<CloneTaskModal
cronJob={jobToClone}
isOpen={isCloneModalOpen}
onClose={() => setIsCloneModalOpen(false)}
onConfirm={handleCloneLocal}
isCloneModalOpen={isCloneModalOpen}
onCloneModalClose={() => setIsCloneModalOpen(false)}
onCloneConfirm={handleCloneLocal}
jobToClone={jobToClone}
isCloning={isCloning}
/>
{errorModalOpen && selectedError && (
<ErrorDetailsModal
isOpen={errorModalOpen}
onClose={() => {
setErrorModalOpen(false);
setSelectedError(null);
}}
error={{
title: selectedError.title,
message: selectedError.message,
details: selectedError.details,
command: selectedError.command,
output: selectedError.output,
stderr: selectedError.stderr,
timestamp: selectedError.timestamp,
jobId: selectedError.jobId,
}}
/>
)}
isErrorModalOpen={errorModalOpen}
onErrorModalClose={() => {
setErrorModalOpen(false);
setSelectedError(null);
}}
selectedError={selectedError}
/>
</>
);
};
};

View File

@@ -0,0 +1,40 @@
"use client";
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
import { Clock, Plus } from "lucide-react";
interface CronJobEmptyStateProps {
selectedUser: string | null;
onNewTaskClick: () => void;
}
export const CronJobEmptyState = ({
selectedUser,
onNewTaskClick,
}: CronJobEmptyStateProps) => {
return (
<div className="text-center py-16">
<div className="mx-auto w-20 h-20 bg-gradient-to-br from-primary/20 to-blue-500/20 rounded-full flex items-center justify-center mb-6">
<Clock className="h-10 w-10 text-primary" />
</div>
<h3 className="text-xl font-semibold mb-3 brand-gradient">
{selectedUser
? `No tasks for user ${selectedUser}`
: "No scheduled tasks yet"}
</h3>
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
{selectedUser
? `No scheduled tasks found for user ${selectedUser}. Try selecting a different user or create a new task.`
: "Create your first scheduled task to automate your system operations and boost productivity."}
</p>
<Button
onClick={onNewTaskClick}
className="btn-primary glow-primary"
size="lg"
>
<Plus className="h-5 w-5 mr-2" />
Create Your First Task
</Button>
</div>
);
};

View File

@@ -0,0 +1,198 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
import {
Trash2,
Edit,
Files,
User,
Play,
Pause,
Code,
Info,
} from "lucide-react";
import { CronJob } from "@/app/_utils/cronjob-utils";
import { JobError } from "@/app/_utils/error-utils";
import { ErrorBadge } from "@/app/_components/GlobalComponents/Badges/ErrorBadge";
import { parseCronExpression, type CronExplanation } from "@/app/_utils/parser-utils";
import { useLocale } from "next-intl";
import { useTranslations } from "next-intl";
interface CronJobItemProps {
job: CronJob;
errors: JobError[];
runningJobId: string | null;
deletingId: string | null;
onRun: (id: string) => void;
onEdit: (job: CronJob) => void;
onClone: (job: CronJob) => void;
onResume: (id: string) => void;
onPause: (id: string) => void;
onDelete: (job: CronJob) => void;
onErrorClick: (error: JobError) => void;
onErrorDismiss: () => void;
}
export const CronJobItem = ({
job,
errors,
runningJobId,
deletingId,
onRun,
onEdit,
onClone,
onResume,
onPause,
onDelete,
onErrorClick,
onErrorDismiss,
}: CronJobItemProps) => {
const [cronExplanation, setCronExplanation] = useState<CronExplanation | null>(null);
const locale = useLocale();
const t = useTranslations();
useEffect(() => {
if (job.schedule) {
const explanation = parseCronExpression(job.schedule, locale);
setCronExplanation(explanation);
} else {
setCronExplanation(null);
}
}, [job.schedule]);
return (
<div
key={job.id}
className="glass-card p-4 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors"
>
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
<div className="flex-1 min-w-0 order-2 lg:order-1">
<div className="flex items-center gap-3 mb-2">
<code className="text-sm bg-purple-500/10 text-purple-600 dark:text-purple-400 px-2 py-1 rounded font-mono border border-purple-500/20">
{job.schedule}
</code>
<div className="flex-1 min-w-0">
<pre
className="text-sm font-medium text-foreground truncate bg-muted/30 px-2 py-1 rounded border border-border/30"
title={job.command}
>
{job.command}
</pre>
</div>
</div>
{cronExplanation?.isValid && (
<div className="flex items-start gap-1.5 mb-1">
<Info className="h-3 w-3 text-primary mt-0.5 flex-shrink-0" />
<p className="text-xs text-muted-foreground italic">
{cronExplanation.humanReadable}
</p>
</div>
)}
<div className="flex items-center gap-2 mb-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<User className="h-3 w-3" />
<span>{job.user}</span>
</div>
{job.paused && (
<span className="text-xs bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 px-2 py-0.5 rounded border border-yellow-500/20">
{t("cronjobs.paused")}
</span>
)}
<ErrorBadge
errors={errors}
onErrorClick={onErrorClick}
onErrorDismiss={onErrorDismiss}
/>
</div>
{job.comment && (
<p
className="text-xs text-muted-foreground italic truncate"
title={job.comment}
>
{job.comment}
</p>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0 order-1 lg:order-2">
<Button
variant="outline"
size="sm"
onClick={() => onRun(job.id)}
disabled={runningJobId === job.id || job.paused}
className="btn-outline h-8 px-3"
title={t("cronjobs.runCronManually")}
aria-label={t("cronjobs.runCronManually")}
>
{runningJobId === job.id ? (
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
<Code className="h-3 w-3" />
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onEdit(job)}
className="btn-outline h-8 px-3"
title={t("cronjobs.editCronJob")}
aria-label={t("cronjobs.editCronJob")}
>
<Edit className="h-3 w-3" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onClone(job)}
className="btn-outline h-8 px-3"
title={t("cronjobs.cloneCronJob")}
aria-label={t("cronjobs.cloneCronJob")}
>
<Files className="h-3 w-3" />
</Button>
{job.paused ? (
<Button
variant="outline"
size="sm"
onClick={() => onResume(job.id)}
className="btn-outline h-8 px-3"
title={t("cronjobs.resumeCronJob")}
aria-label={t("cronjobs.resumeCronJob")}
>
<Play className="h-3 w-3" />
</Button>
) : (
<Button
variant="outline"
size="sm"
onClick={() => onPause(job.id)}
className="btn-outline h-8 px-3"
title={t("cronjobs.pauseCronJob")}
aria-label={t("cronjobs.pauseCronJob")}
>
<Pause className="h-3 w-3" />
</Button>
)}
<Button
variant="destructive"
size="sm"
onClick={() => onDelete(job)}
disabled={deletingId === job.id}
className="btn-destructive h-8 px-3"
title={t("cronjobs.deleteCronJob")}
aria-label={t("cronjobs.deleteCronJob")}
>
{deletingId === job.id ? (
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : (
<Trash2 className="h-3 w-3" />
)}
</Button>
</div>
</div>
</div>
);
};

View File

@@ -11,10 +11,10 @@ import {
HardDrive,
Wifi,
} from "lucide-react";
import { useTranslations } from "next-intl";
export interface SidebarProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
title?: string;
defaultCollapsed?: boolean;
quickStats?: {
cpu: number;
@@ -28,13 +28,13 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
{
className,
children,
title = "System Overview",
defaultCollapsed = false,
quickStats,
...props
},
ref
) => {
const t = useTranslations();
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [isMobileOpen, setIsMobileOpen] = useState(false);
@@ -113,7 +113,7 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
</div>
{(!isCollapsed || !isCollapsed) && (
<h2 className="text-sm font-semibold text-foreground truncate">
{title}
{t("sidebar.systemOverview")}
</h2>
)}
</div>

View File

@@ -6,6 +6,7 @@ import { ScriptsManager } from "@/app/_components/FeatureComponents/Scripts/Scri
import { CronJob } from "@/app/_utils/cronjob-utils";
import { Script } from "@/app/_utils/scripts-utils";
import { Clock, FileText } from "lucide-react";
import { useTranslations } from "next-intl";
interface TabbedInterfaceProps {
cronJobs: CronJob[];
@@ -19,6 +20,7 @@ export const TabbedInterface = ({
const [activeTab, setActiveTab] = useState<"cronjobs" | "scripts">(
"cronjobs"
);
const t = useTranslations();
return (
<div className="space-y-6">
@@ -32,7 +34,7 @@ export const TabbedInterface = ({
}`}
>
<Clock className="h-4 w-4" />
Cron Jobs
{t("cronjobs.cronJobs")}
<span className="ml-1 text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full font-medium">
{cronJobs.length}
</span>
@@ -45,7 +47,7 @@ export const TabbedInterface = ({
}`}
>
<FileText className="h-4 w-4" />
Scripts
{t("scripts.scripts")}
<span className="ml-1 text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full font-medium">
{scripts.length}
</span>

View File

@@ -10,6 +10,7 @@ import { UserSwitcher } from "@/app/_components/FeatureComponents/User/UserSwitc
import { Plus, Terminal, FileText, X } from "lucide-react";
import { getScriptContent } from "@/app/_server/actions/scripts";
import { getHostScriptPath } from "@/app/_server/actions/scripts";
import { useTranslations } from "next-intl";
interface Script {
id: string;
@@ -46,6 +47,7 @@ export const CreateTaskModal = ({
useState<string>("");
const [isSelectScriptModalOpen, setIsSelectScriptModalOpen] = useState(false);
const selectedScript = scripts.find((s) => s.id === form.selectedScriptId);
const t = useTranslations();
useEffect(() => {
const loadScriptContent = async () => {
@@ -86,13 +88,13 @@ export const CreateTaskModal = ({
<Modal
isOpen={isOpen}
onClose={onClose}
title="Create New Scheduled Task"
title={t("cronjobs.createNewScheduledTask")}
size="lg"
>
<form onSubmit={onSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-foreground mb-1">
User
{t("common.user")}
</label>
<UserSwitcher
selectedUser={form.user}
@@ -102,7 +104,7 @@ export const CreateTaskModal = ({
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Schedule
{t("cronjobs.schedule")}
</label>
<CronExpressionHelper
value={form.schedule}
@@ -114,7 +116,7 @@ export const CreateTaskModal = ({
<div>
<label className="block text-sm font-medium text-foreground mb-2">
Task Type
{t("cronjobs.taskType")}
</label>
<div className="grid grid-cols-2 gap-3">
<button
@@ -128,8 +130,8 @@ export const CreateTaskModal = ({
<div className="flex items-center gap-3">
<Terminal className="h-5 w-5" />
<div className="text-left">
<div className="font-medium">Custom Command</div>
<div className="text-xs opacity-70">Single command</div>
<div className="font-medium">{t("cronjobs.customCommand")}</div>
<div className="text-xs opacity-70">{t("cronjobs.singleCommand")}</div>
</div>
</div>
</button>
@@ -145,9 +147,9 @@ export const CreateTaskModal = ({
<div className="flex items-center gap-3">
<FileText className="h-5 w-5" />
<div className="text-left">
<div className="font-medium">Saved Script</div>
<div className="font-medium">{t("scripts.savedScript")}</div>
<div className="text-xs opacity-70">
Select from library
{t("scripts.selectFromLibrary")}
</div>
</div>
</div>
@@ -182,7 +184,7 @@ export const CreateTaskModal = ({
onClick={() => setIsSelectScriptModalOpen(true)}
className="h-8 px-2 text-xs"
>
Change
{t("common.change")}
</Button>
<Button
type="button"
@@ -201,7 +203,7 @@ export const CreateTaskModal = ({
{!form.selectedScriptId && !selectedScript && (
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Command
{t("cronjobs.command")}
</label>
<div className="relative">
<textarea
@@ -222,8 +224,7 @@ export const CreateTaskModal = ({
</div>
{form.selectedScriptId && (
<p className="text-xs text-muted-foreground mt-1">
Script path is read-only. Edit the script in the Scripts
Library.
{t("scripts.scriptPathReadOnly")}
</p>
)}
</div>
@@ -231,13 +232,13 @@ export const CreateTaskModal = ({
<div>
<label className="block text-sm font-medium text-foreground mb-1">
Description{" "}
<span className="text-muted-foreground">(Optional)</span>
{t("common.description")}
<span className="text-muted-foreground">({t("common.optional")})</span>
</label>
<Input
value={form.comment}
onChange={(e) => onFormChange({ comment: e.target.value })}
placeholder="What does this task do?"
placeholder={t("cronjobs.whatDoesThisTaskDo")}
className="bg-muted/30 border-border/50 focus:border-primary/50"
/>
</div>
@@ -249,11 +250,11 @@ export const CreateTaskModal = ({
onClick={onClose}
className="btn-outline"
>
Cancel
{t("common.cancel")}
</Button>
<Button type="submit" className="btn-primary glow-primary">
<Plus className="h-4 w-4 mr-2" />
Create Task
{t("cronjobs.createTask")}
</Button>
</div>
</form>

View File

@@ -0,0 +1,121 @@
"use client";
import { CreateTaskModal } from "@/app/_components/FeatureComponents/Modals/CreateTaskModal";
import { EditTaskModal } from "@/app/_components/FeatureComponents/Modals/EditTaskModal";
import { DeleteTaskModal } from "@/app/_components/FeatureComponents/Modals/DeleteTaskModal";
import { CloneTaskModal } from "@/app/_components/FeatureComponents/Modals/CloneTaskModal";
import { ErrorDetailsModal } from "@/app/_components/FeatureComponents/Modals/ErrorDetailsModal";
import { CronJob } from "@/app/_utils/cronjob-utils";
import { Script } from "@/app/_utils/scripts-utils";
import { JobError } from "@/app/_utils/error-utils";
interface CronJobListModalsProps {
cronJobs: CronJob[];
scripts: Script[];
isNewCronModalOpen: boolean;
onNewCronModalClose: () => void;
onNewCronSubmit: (e: React.FormEvent) => Promise<void>;
newCronForm: any;
onNewCronFormChange: (updates: any) => void;
isEditModalOpen: boolean;
onEditModalClose: () => void;
onEditSubmit: (e: React.FormEvent) => Promise<void>;
editForm: any;
onEditFormChange: (updates: any) => void;
isDeleteModalOpen: boolean;
onDeleteModalClose: () => void;
onDeleteConfirm: () => void;
jobToDelete: CronJob | null;
isCloneModalOpen: boolean;
onCloneModalClose: () => void;
onCloneConfirm: (newComment: string) => Promise<void>;
jobToClone: CronJob | null;
isCloning: boolean;
isErrorModalOpen: boolean;
onErrorModalClose: () => void;
selectedError: JobError | null;
}
export const CronJobListModals = ({
scripts,
isNewCronModalOpen,
onNewCronModalClose,
onNewCronSubmit,
newCronForm,
onNewCronFormChange,
isEditModalOpen,
onEditModalClose,
onEditSubmit,
editForm,
onEditFormChange,
isDeleteModalOpen,
onDeleteModalClose,
onDeleteConfirm,
jobToDelete,
isCloneModalOpen,
onCloneModalClose,
onCloneConfirm,
jobToClone,
isCloning,
isErrorModalOpen,
onErrorModalClose,
selectedError,
}: CronJobListModalsProps) => {
return (
<>
<CreateTaskModal
isOpen={isNewCronModalOpen}
onClose={onNewCronModalClose}
onSubmit={onNewCronSubmit}
scripts={scripts}
form={newCronForm}
onFormChange={onNewCronFormChange}
/>
<EditTaskModal
isOpen={isEditModalOpen}
onClose={onEditModalClose}
onSubmit={onEditSubmit}
form={editForm}
onFormChange={onEditFormChange}
/>
<DeleteTaskModal
isOpen={isDeleteModalOpen}
onClose={onDeleteModalClose}
onConfirm={onDeleteConfirm}
job={jobToDelete}
/>
<CloneTaskModal
cronJob={jobToClone}
isOpen={isCloneModalOpen}
onClose={onCloneModalClose}
onConfirm={onCloneConfirm}
isCloning={isCloning}
/>
{isErrorModalOpen && selectedError && (
<ErrorDetailsModal
isOpen={isErrorModalOpen}
onClose={onErrorModalClose}
error={{
title: selectedError.title,
message: selectedError.message,
details: selectedError.details,
command: selectedError.command,
output: selectedError.output,
stderr: selectedError.stderr,
timestamp: selectedError.timestamp,
jobId: selectedError.jobId,
}}
/>
)}
</>
);
};

View File

@@ -8,6 +8,7 @@ import { FileText, Search, Check, Terminal } from "lucide-react";
import { Script } from "@/app/_utils/scripts-utils";
import { getScriptContent } from "@/app/_server/actions/scripts";
import { getHostScriptPath } from "@/app/_server/actions/scripts";
import { useTranslations } from "next-intl";
interface SelectScriptModalProps {
isOpen: boolean;
@@ -24,6 +25,7 @@ export const SelectScriptModal = ({
onScriptSelect,
selectedScriptId,
}: SelectScriptModalProps) => {
const t = useTranslations();
const [searchQuery, setSearchQuery] = useState("");
const [previewScript, setPreviewScript] = useState<Script | null>(null);
const [previewContent, setPreviewContent] = useState<string>("");
@@ -77,7 +79,7 @@ export const SelectScriptModal = ({
<Modal
isOpen={isOpen}
onClose={handleClose}
title="Select Script"
title={t("scripts.selectScript")}
size="xl"
>
<div className="space-y-4">
@@ -86,7 +88,7 @@ export const SelectScriptModal = ({
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search scripts..."
placeholder={t("scripts.searchScripts")}
className="pl-10"
/>
</div>
@@ -95,13 +97,13 @@ export const SelectScriptModal = ({
<div className="border border-border rounded-lg overflow-hidden">
<div className="bg-muted/30 px-4 py-2 border-b border-border">
<h3 className="text-sm font-medium text-foreground">
Available Scripts ({filteredScripts.length})
{t("scripts.availableScripts", { count: filteredScripts.length })}
</h3>
</div>
<div className="overflow-y-auto h-full max-h-80">
{filteredScripts.length === 0 ? (
<div className="p-4 text-center text-muted-foreground">
{searchQuery ? "No scripts found" : "No scripts available"}
{searchQuery ? t("scripts.noScriptsFound") : t("scripts.noScriptsAvailable")}
</div>
) : (
<div className="divide-y divide-border">
@@ -143,7 +145,7 @@ export const SelectScriptModal = ({
<div className="border border-border rounded-lg overflow-hidden">
<div className="bg-muted/30 px-4 py-2 border-b border-border">
<h3 className="text-sm font-medium text-foreground">
Script Preview
{t("scripts.scriptPreview")}
</h3>
</div>
<div className="p-4 h-full max-h-80 overflow-y-auto">
@@ -162,7 +164,7 @@ export const SelectScriptModal = ({
<div className="flex items-center gap-2 mb-2">
<Terminal className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-foreground">
Command Preview
{t("scripts.commandPreview")}
</span>
</div>
<div className="bg-muted/30 p-3 rounded border border-border/30">
@@ -174,7 +176,7 @@ export const SelectScriptModal = ({
<div>
<span className="text-sm font-medium text-foreground">
Script Content
{t("scripts.scriptContent")}
</span>
<div className="bg-muted/30 p-3 rounded border border-border/30 mt-2 max-h-32 overflow-auto">
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap">
@@ -186,7 +188,7 @@ export const SelectScriptModal = ({
) : (
<div className="text-center text-muted-foreground py-8">
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Select a script to preview</p>
<p>{t("scripts.selectScriptToPreview")}</p>
</div>
)}
</div>
@@ -200,7 +202,7 @@ export const SelectScriptModal = ({
onClick={handleClose}
className="btn-outline"
>
Cancel
{t("common.cancel")}
</Button>
<Button
type="button"
@@ -209,7 +211,7 @@ export const SelectScriptModal = ({
className="btn-primary glow-primary"
>
<Check className="h-4 w-4 mr-2" />
Select Script
{t("scripts.selectScript")}
</Button>
</div>
</div>

View File

@@ -18,6 +18,7 @@ import {
ChevronUp,
Search,
} from "lucide-react";
import { useLocale } from "next-intl";
interface CronExpressionHelperProps {
value: string;
@@ -34,6 +35,7 @@ export const CronExpressionHelper = ({
className = "",
showPatterns = true,
}: CronExpressionHelperProps) => {
const locale = useLocale();
const [explanation, setExplanation] = useState<CronExplanation | null>(null);
const [showPatternsPanel, setShowPatternsPanel] = useState(false);
const [debouncedValue, setDebouncedValue] = useState(value);
@@ -49,7 +51,7 @@ export const CronExpressionHelper = ({
useEffect(() => {
if (debouncedValue) {
const result = parseCronExpression(debouncedValue);
const result = parseCronExpression(debouncedValue, locale);
setExplanation(result);
} else {
setExplanation(null);

View File

@@ -26,6 +26,7 @@ import { EditScriptModal } from "@/app/_components/FeatureComponents/Modals/Edit
import { DeleteScriptModal } from "@/app/_components/FeatureComponents/Modals/DeleteScriptModal";
import { CloneScriptModal } from "@/app/_components/FeatureComponents/Modals/CloneScriptModal";
import { showToast } from "@/app/_components/GlobalComponents/UIElements/Toast";
import { useTranslations } from "next-intl";
interface ScriptsManagerProps {
scripts: Script[];
@@ -43,6 +44,7 @@ export const ScriptsManager = ({
const [copiedId, setCopiedId] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [isCloning, setIsCloning] = useState(false);
const t = useTranslations();
const [createForm, setCreateForm] = useState({
name: "",
@@ -173,10 +175,10 @@ export const ScriptsManager = ({
</div>
<div>
<CardTitle className="text-xl brand-gradient">
Scripts Library
{t("scripts.scriptsLibrary")}
</CardTitle>
<p className="text-sm text-muted-foreground">
{scripts.length} saved script{scripts.length !== 1 ? "s" : ""}
{t("scripts.nOfNSavedScripts", { count: scripts.length })}
</p>
</div>
</div>
@@ -185,7 +187,7 @@ export const ScriptsManager = ({
className="btn-primary glow-primary"
>
<Plus className="h-4 w-4 mr-2" />
New Script
{t("scripts.newScript")}
</Button>
</div>
</CardHeader>
@@ -196,10 +198,10 @@ export const ScriptsManager = ({
<FileText className="h-10 w-10 text-primary" />
</div>
<h3 className="text-xl font-semibold mb-3 brand-gradient">
No scripts yet
{t("scripts.noScriptsYet")}
</h3>
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
Create reusable bash scripts to use in your scheduled tasks.
{t("scripts.createReusableBashScripts")}
</p>
<Button
onClick={() => setIsCreateModalOpen(true)}
@@ -207,7 +209,7 @@ export const ScriptsManager = ({
size="lg"
>
<Plus className="h-5 w-5 mr-2" />
Create Your First Script
{t("scripts.createYourFirstScript")}
</Button>
</div>
) : (
@@ -233,7 +235,7 @@ export const ScriptsManager = ({
</p>
)}
<div className="text-xs text-muted-foreground">
File: {script.filename}
{t("scripts.file")}: {script.filename}
</div>
</div>

View File

@@ -55,6 +55,7 @@ interface SystemInfoType {
};
}
import { useState, useEffect } from "react";
import { useTranslations } from "next-intl";
interface SystemInfoCardProps {
systemInfo: SystemInfoType;
@@ -67,6 +68,7 @@ export const SystemInfoCard = ({
const [systemInfo, setSystemInfo] =
useState<SystemInfoType>(initialSystemInfo);
const [isUpdating, setIsUpdating] = useState(false);
const t = useTranslations();
@@ -126,7 +128,7 @@ export const SystemInfoCard = ({
const basicInfoItems = [
{
icon: Clock,
label: "Uptime",
label: t("sidebar.uptime"),
value: systemInfo.uptime,
color: "text-orange-500",
},
@@ -135,7 +137,7 @@ export const SystemInfoCard = ({
const performanceItems = [
{
icon: HardDrive,
label: "Memory",
label: t("sidebar.memory"),
value: `${systemInfo.memory.used} / ${systemInfo.memory.total}`,
detail: `${systemInfo.memory.free} free`,
status: systemInfo.memory.status,
@@ -145,7 +147,7 @@ export const SystemInfoCard = ({
},
{
icon: Cpu,
label: "CPU",
label: t("sidebar.cpu"),
value: systemInfo.cpu.model,
detail: `${systemInfo.cpu.cores} cores`,
status: systemInfo.cpu.status,
@@ -155,7 +157,7 @@ export const SystemInfoCard = ({
},
{
icon: Monitor,
label: "GPU",
label: t("sidebar.gpu"),
value: systemInfo.gpu.model,
detail: systemInfo.gpu.memory
? `${systemInfo.gpu.memory} VRAM`
@@ -165,7 +167,7 @@ export const SystemInfoCard = ({
},
...(systemInfo.network ? [{
icon: Wifi,
label: "Network",
label: t("sidebar.network"),
value: `${systemInfo.network.latency}ms`,
detail: `${systemInfo.network.latency}ms latency • ${systemInfo.network.speed}`,
status: systemInfo.network.status,
@@ -175,17 +177,17 @@ export const SystemInfoCard = ({
const performanceMetrics = [
{
label: "CPU Usage",
label: t("sidebar.cpuUsage"),
value: `${systemInfo.cpu.usage}%`,
status: systemInfo.cpu.status,
},
{
label: "Memory Usage",
label: t("sidebar.memoryUsage"),
value: `${systemInfo.memory.usage}%`,
status: systemInfo.memory.status,
},
...(systemInfo.network ? [{
label: "Network Latency",
label: t("sidebar.networkLatency"),
value: `${systemInfo.network.latency}ms`,
status: systemInfo.network.status,
}] : []),
@@ -193,7 +195,6 @@ export const SystemInfoCard = ({
return (
<Sidebar
title="System Overview"
defaultCollapsed={false}
quickStats={quickStats}
>
@@ -206,7 +207,7 @@ export const SystemInfoCard = ({
<div>
<h3 className="text-xs font-semibold text-foreground mb-2 uppercase tracking-wide">
System Information
{t("sidebar.systemInformation")}
</h3>
<div className="space-y-2">
{basicInfoItems.map((item) => (
@@ -224,7 +225,7 @@ export const SystemInfoCard = ({
<div>
<h3 className="text-xs font-semibold text-foreground mb-2 uppercase tracking-wide">
Performance Metrics
{t("sidebar.performanceMetrics")}
</h3>
<div className="space-y-2">
{performanceItems.map((item) => (
@@ -247,14 +248,14 @@ export const SystemInfoCard = ({
<PerformanceSummary metrics={performanceMetrics} />
<div className="text-xs text-muted-foreground text-center p-2 bg-muted/20 rounded-lg">
💡 Stats update every{" "}
{t("sidebar.statsUpdateEvery")}{" "}
{Math.round(
parseInt(process.env.NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL || "30000") /
1000
)}
s Network speed estimated from latency
s {t("sidebar.networkSpeedEstimatedFromLatency")}
{isUpdating && (
<span className="ml-2 animate-pulse">🔄 Updating...</span>
<span className="ml-2 animate-pulse">{t("sidebar.updating")}...</span>
)}
</div>
</Sidebar>

View File

@@ -1,6 +1,7 @@
import { cn } from "@/app/_utils/global-utils";
import { HTMLAttributes, forwardRef } from "react";
import { Activity } from "lucide-react";
import { useTranslations } from "next-intl";
export interface SystemStatusProps extends HTMLAttributes<HTMLDivElement> {
status: string;
@@ -14,6 +15,7 @@ export const SystemStatus = forwardRef<HTMLDivElement, SystemStatusProps>(
{ className, status, details, timestamp, isUpdating = false, ...props },
ref
) => {
const t = useTranslations();
const getStatusConfig = (status: string) => {
const lowerStatus = status.toLowerCase();
@@ -64,11 +66,11 @@ export const SystemStatus = forwardRef<HTMLDivElement, SystemStatusProps>(
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium text-foreground">
System Status: {status}
{t("system.systemStatus")}: {status}
</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
{details} Last updated: {timestamp}
{details} {t("system.lastUpdated")}: {timestamp}
{isUpdating && <span className="ml-2 animate-pulse">🔄</span>}
</p>
</div>

View File

@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
import { ChevronDown, User, X } from "lucide-react";
import { fetchAvailableUsers } from "@/app/_server/actions/cronjobs";
import { useTranslations } from "next-intl";
interface UserFilterProps {
selectedUser: string | null;
@@ -19,6 +20,7 @@ export const UserFilter = ({
const [users, setUsers] = useState<string[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const t = useTranslations();
useEffect(() => {
const loadUsers = async () => {
@@ -56,7 +58,7 @@ export const UserFilter = ({
<div className="flex items-center gap-2">
<User className="h-4 w-4" />
<span className="text-sm">
{selectedUser ? `User: ${selectedUser}` : "All users"}
{selectedUser ? `${t("common.userWithUsername", { user: selectedUser })}` : t("common.allUsers")}
</span>
</div>
<div className="flex items-center gap-1">
@@ -85,7 +87,7 @@ export const UserFilter = ({
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${!selectedUser ? "bg-accent text-accent-foreground" : ""
}`}
>
All users
{t("common.allUsers")}
</button>
{users.map((user) => (
<button

View File

@@ -1,6 +1,7 @@
import { cn } from "@/app/_utils/global-utils";
import { HTMLAttributes, forwardRef } from "react";
import { CheckCircle, AlertTriangle, XCircle, Activity } from "lucide-react";
import { useTranslations } from "next-intl";
export interface StatusBadgeProps extends HTMLAttributes<HTMLDivElement> {
status: string;
@@ -21,6 +22,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
},
ref
) => {
const t = useTranslations();
const getStatusConfig = (status: string) => {
const lowerStatus = status.toLowerCase();
@@ -33,7 +35,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
bgColor: "bg-emerald-500/10",
borderColor: "border-emerald-500/20",
icon: CheckCircle,
label: "Optimal",
label: t("system.optimal"),
};
case "moderate":
case "warning":
@@ -42,7 +44,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
bgColor: "bg-yellow-500/10",
borderColor: "border-yellow-500/20",
icon: AlertTriangle,
label: "Warning",
label: t("system.warning"),
};
case "high":
case "slow":
@@ -51,7 +53,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
bgColor: "bg-orange-500/10",
borderColor: "border-orange-500/20",
icon: AlertTriangle,
label: "High",
label: t("system.high"),
};
case "critical":
case "poor":
@@ -61,7 +63,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
bgColor: "bg-destructive/10",
borderColor: "border-destructive/20",
icon: XCircle,
label: "Critical",
label: t("system.critical"),
};
default:
return {
@@ -69,7 +71,7 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
bgColor: "bg-muted",
borderColor: "border-border",
icon: Activity,
label: "Unknown",
label: t("system.unknown"),
};
}
};

4
app/_consts/global.ts Normal file
View File

@@ -0,0 +1,4 @@
export const Locales = [
{ locale: "en", label: "English" },
{ locale: "it", label: "Italian" },
];

View File

@@ -0,0 +1,202 @@
"use client";
import { useState, useMemo, useEffect } from "react";
import { CronJob } from "@/app/_utils/cronjob-utils";
import { Script } from "@/app/_utils/scripts-utils";
import {
getJobErrorsByJobId,
JobError,
} from "@/app/_utils/error-utils";
import {
handleErrorClick,
handleDelete,
handleClone,
handlePause,
handleResume,
handleRun,
handleEditSubmit,
handleNewCronSubmit,
} from "@/app/_components/FeatureComponents/Cronjobs/helpers";
interface CronJobListProps {
cronJobs: CronJob[];
scripts: Script[];
}
export const useCronJobState = ({ cronJobs, scripts }: CronJobListProps) => {
const [deletingId, setDeletingId] = useState<string | null>(null);
const [editingJob, setEditingJob] = useState<CronJob | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isNewCronModalOpen, setIsNewCronModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isCloneModalOpen, setIsCloneModalOpen] = useState(false);
const [jobToDelete, setJobToDelete] = useState<CronJob | null>(null);
const [jobToClone, setJobToClone] = useState<CronJob | null>(null);
const [isCloning, setIsCloning] = useState(false);
const [runningJobId, setRunningJobId] = useState<string | null>(null);
const [selectedUser, setSelectedUser] = useState<string | null>(null);
const [jobErrors, setJobErrors] = useState<Record<string, JobError[]>>({});
const [errorModalOpen, setErrorModalOpen] = useState(false);
const [selectedError, setSelectedError] = useState<JobError | null>(null);
const [editForm, setEditForm] = useState({
schedule: "",
command: "",
comment: "",
});
const [newCronForm, setNewCronForm] = useState({
schedule: "",
command: "",
comment: "",
selectedScriptId: null as string | null,
user: "",
});
useEffect(() => {
const savedUser = localStorage.getItem("selectedCronUser");
if (savedUser) {
setSelectedUser(savedUser);
}
}, []);
useEffect(() => {
if (selectedUser) {
localStorage.setItem("selectedCronUser", selectedUser);
} else {
localStorage.removeItem("selectedCronUser");
}
}, [selectedUser]);
const filteredJobs = useMemo(() => {
if (!selectedUser) return cronJobs;
return cronJobs.filter((job) => job.user === selectedUser);
}, [cronJobs, selectedUser]);
useEffect(() => {
const errors: Record<string, JobError[]> = {};
filteredJobs.forEach((job) => {
errors[job.id] = getJobErrorsByJobId(job.id);
});
setJobErrors(errors);
}, [filteredJobs]);
const refreshJobErrorsLocal = () => {
const errors: Record<string, JobError[]> = {};
filteredJobs.forEach((job) => {
errors[job.id] = getJobErrorsByJobId(job.id);
});
setJobErrors(errors);
};
const getHelperState = () => ({
setDeletingId,
setIsDeleteModalOpen,
setJobToDelete,
setIsCloneModalOpen,
setJobToClone,
setIsCloning,
setIsEditModalOpen,
setEditingJob,
setIsNewCronModalOpen,
setNewCronForm,
setRunningJobId,
refreshJobErrors: refreshJobErrorsLocal,
jobToClone,
editingJob,
editForm,
newCronForm,
});
const handleErrorClickLocal = (error: JobError) => {
handleErrorClick(error, setSelectedError, setErrorModalOpen);
};
const handleDeleteLocal = async (id: string) => {
await handleDelete(id, getHelperState());
};
const handleCloneLocal = async (newComment: string) => {
await handleClone(newComment, getHelperState());
};
const handlePauseLocal = async (id: string) => {
await handlePause(id);
};
const handleResumeLocal = async (id: string) => {
await handleResume(id);
};
const handleRunLocal = async (id: string) => {
await handleRun(id, getHelperState());
};
const confirmDelete = (job: CronJob) => {
setJobToDelete(job);
setIsDeleteModalOpen(true);
};
const confirmClone = (job: CronJob) => {
setJobToClone(job);
setIsCloneModalOpen(true);
};
const handleEdit = (job: CronJob) => {
setEditingJob(job);
setEditForm({
schedule: job.schedule,
command: job.command,
comment: job.comment || "",
});
setIsEditModalOpen(true);
};
const handleEditSubmitLocal = async (e: React.FormEvent) => {
await handleEditSubmit(e, getHelperState());
};
const handleNewCronSubmitLocal = async (e: React.FormEvent) => {
await handleNewCronSubmit(e, getHelperState());
};
return {
deletingId,
runningJobId,
selectedUser,
setSelectedUser,
jobErrors,
errorModalOpen,
setErrorModalOpen,
selectedError,
setSelectedError,
filteredJobs,
isNewCronModalOpen,
setIsNewCronModalOpen,
isEditModalOpen,
setIsEditModalOpen,
isDeleteModalOpen,
setIsDeleteModalOpen,
isCloneModalOpen,
setIsCloneModalOpen,
jobToDelete,
jobToClone,
isCloning,
editForm,
setEditForm,
newCronForm,
setNewCronForm,
handleErrorClickLocal,
refreshJobErrorsLocal,
handleDeleteLocal,
handleCloneLocal,
handlePauseLocal,
handleResumeLocal,
handleRunLocal,
confirmDelete,
confirmClone,
handleEdit,
handleEditSubmitLocal,
handleNewCronSubmitLocal,
};
};

94
app/_translations/en.json Normal file
View File

@@ -0,0 +1,94 @@
{
"common": {
"cronManagementMadeEasy": "Cron Management made easy",
"allUsers": "All users",
"userWithUsername": "User: {user}",
"user": "User",
"change": "Change",
"description": "Description",
"optional": "Optional",
"cancel": "Cancel"
},
"cronjobs": {
"cronJobs": "Cron Jobs",
"cronJob": "Cron Job",
"scheduledTasks": "Scheduled Tasks",
"nOfNJObs": "{filtered} of {total} scheduled tasks",
"forUser": "for user {user}",
"newTask": "New Task",
"runCronManually": "Run cron job manually",
"editCronJob": "Edit cron job",
"cloneCronJob": "Clone cron job",
"deleteCronJob": "Delete cron job",
"pauseCronJob": "Pause cron job",
"resumeCronJob": "Resume cron job",
"runCronJob": "Run cron job",
"runCronJobSuccess": "Cron job executed successfully",
"runCronJobFailed": "Failed to execute cron job",
"paused": "Paused",
"createNewScheduledTask": "Create new scheduled task",
"schedule": "Schedule",
"taskType": "Task Type",
"customCommand": "Custom Command",
"singleCommand": "Single command",
"command": "Command",
"whatDoesThisTaskDo": "What does this task do?",
"createTask": "Create Task"
},
"scripts": {
"scripts": "Scripts",
"scriptsLibrary": "Scripts Library",
"file": "File",
"newScript": "New Script",
"noScriptsYet": "No scripts yet",
"createReusableBashScripts": "Create reusable bash scripts to use in your scheduled tasks.",
"createYourFirstScript": "Create Your First Script",
"nOfNSavedScripts": "{count} saved scripts",
"savedScript": "Saved Script",
"selectFromLibrary": "Select from library",
"scriptPathReadOnly": "Script path is read-only. Edit the script in the Scripts Library",
"selectScript": "Select Script",
"availableScripts": "{count} available scripts",
"noScriptsFound": "No scripts found",
"noScriptsAvailable": "No scripts available",
"scriptPreview": "Script Preview",
"commandPreview": "Command Preview",
"scriptContent": "Script Content",
"selectScriptToPreview": "Select a script to preview",
"searchScripts": "Search scripts..."
},
"sidebar": {
"systemOverview": "System Overview",
"uptime": "Uptime",
"memory": "Memory",
"cpu": "CPU",
"gpu": "GPU",
"network": "Network",
"networkLatency": "Network Latency",
"memoryUsage": "Memory Usage",
"cpuUsage": "CPU Usage",
"systemInformation": "System Information",
"performanceMetrics": "Performance Metrics",
"statsUpdateEvery": "Stats update every",
"updating": "Updating",
"networkSpeedEstimatedFromLatency": "Network speed estimated from latency"
},
"system": {
"optimal": "Optimal",
"critical": "Critical",
"high": "High",
"moderate": "Moderate",
"warning": "Warning",
"unknown": "Unknown",
"connected": "Connected",
"allSystemsRunningNormally": "All systems running normally",
"highResourceUsageDetectedImmediateAttentionRequired": "High resource usage detected - immediate attention required",
"moderateResourceUsageMonitoringRecommended": "Moderate resource usage - monitoring recommended",
"unknownGPU": "Unknown GPU",
"noGPUDetected": "No GPU detected",
"gpuDetectionFailed": "GPU detection failed",
"available": "Available",
"systemStatus": "System Status",
"lastUpdated": "Last updated"
}
}

94
app/_translations/it.json Normal file
View File

@@ -0,0 +1,94 @@
{
"common": {
"cronManagementMadeEasy": "Gestione Cron semplificata",
"allUsers": "Tutti gli utenti",
"userWithUsername": "Utente: {user}",
"user": "Utente",
"change": "Modifica",
"description": "Descrizione",
"optional": "Opzionale",
"cancel": "Annulla"
},
"cronjobs": {
"cronJobs": "Operazioni Cron",
"cronJob": "Operazione Cron",
"scheduledTasks": "Operazioni Pianificate",
"nOfNJObs": "{filtered} di {total} operazioni pianificate",
"forUser": "per l'utente {user}",
"newTask": "Nuova Operazione",
"runCronManually": "Esegui operazione cron manualmente",
"editCronJob": "Modifica operazione cron",
"cloneCronJob": "Clona operazione cron",
"deleteCronJob": "Elimina operazione cron",
"pauseCronJob": "Pausa operazione cron",
"resumeCronJob": "Riprendi operazione cron",
"runCronJob": "Esegui operazione cron",
"runCronJobSuccess": "Operazione cron eseguita con successo",
"runCronJobFailed": "Esecuzione operazione cron fallita",
"paused": "In pausa",
"createNewScheduledTask": "Crea nuova operazione pianificata",
"schedule": "Pianificazione",
"taskType": "Tipo di Operazione",
"customCommand": "Comando Personalizzato",
"singleCommand": "Comando singolo",
"command": "Comando",
"whatDoesThisTaskDo": "Cosa fa questa operazione?",
"createTask": "Crea Operazione"
},
"scripts": {
"scripts": "Script",
"scriptsLibrary": "Libreria Script",
"file": "File",
"newScript": "Nuovo Script",
"noScriptsYet": "Ancora nessuno script",
"createReusableBashScripts": "Crea script bash riutilizzabili da usare nelle tue operazioni pianificate.",
"createYourFirstScript": "Crea il tuo primo script",
"nOfNSavedScripts": "{count} script salvati",
"savedScript": "Script Salvato",
"selectFromLibrary": "Seleziona dalla libreria",
"scriptPathReadOnly": "Il percorso dello script è di sola lettura. Modifica lo script nella Libreria Script",
"selectScript": "Seleziona Script",
"availableScripts": "{count} script disponibili",
"noScriptsFound": "Nessuno script trovato",
"noScriptsAvailable": "Nessuno script disponibile",
"scriptPreview": "Anteprima Script",
"commandPreview": "Anteprima Comando",
"scriptContent": "Contenuto Script",
"selectScriptToPreview": "Seleziona uno script per l'anteprima",
"searchScripts": "Cerca script..."
},
"sidebar": {
"systemOverview": "Panoramica del Sistema",
"uptime": "Uptime",
"memory": "Memoria",
"cpu": "CPU",
"gpu": "GPU",
"network": "Rete",
"networkLatency": "Latenza di Rete",
"memoryUsage": "Utilizzo Memoria",
"cpuUsage": "Utilizzo CPU",
"systemInformation": "Informazioni di Sistema",
"performanceMetrics": "Metriche delle Prestazioni",
"statsUpdateEvery": "Statistiche aggiornate ogni",
"updating": "Aggiornamento",
"networkSpeedEstimatedFromLatency": "Velocità di rete stimata dalla latenza"
},
"system": {
"optimal": "Ottimale",
"critical": "Critico",
"high": "Alto",
"moderate": "Moderato",
"warning": "Avviso",
"unknown": "Sconosciuto",
"connected": "Connesso",
"allSystemsRunningNormally": "Tutti i sistemi funzionano normalmente",
"highResourceUsageDetectedImmediateAttentionRequired": "Rilevato utilizzo elevato delle risorse - richiesta attenzione immediata",
"moderateResourceUsageMonitoringRecommended": "Utilizzo moderato delle risorse - monitoraggio raccomandato",
"unknownGPU": "GPU Sconosciuta",
"noGPUDetected": "Nessuna GPU rilevata",
"gpuDetectionFailed": "Rilevamento GPU fallito",
"available": "Disponibile",
"systemStatus": "Stato del Sistema",
"lastUpdated": "Ultimo aggiornamento"
}
}

View File

@@ -1,4 +1,4 @@
import cronstrue from "cronstrue";
import cronstrue from 'cronstrue/i18n';
export interface CronExplanation {
humanReadable: string;
@@ -7,7 +7,7 @@ export interface CronExplanation {
error?: string;
}
export const parseCronExpression = (expression: string): CronExplanation => {
export const parseCronExpression = (expression: string, locale?: string): CronExplanation => {
try {
const cleanExpression = expression.trim();
@@ -23,6 +23,7 @@ export const parseCronExpression = (expression: string): CronExplanation => {
const humanReadable = cronstrue.toString(cleanExpression, {
verbose: true,
throwExceptionOnParseError: false,
locale: locale || "en",
});
return {

View File

@@ -1,11 +1,14 @@
import { NextRequest, NextResponse } from 'next/server';
import * as si from 'systeminformation';
export const dynamic = 'force-dynamic';
import { getTranslations } from 'next-intl/server';
export async function GET(request: NextRequest) {
try {
const t = await getTranslations();
const [
osInfo,
memInfo,
cpuInfo,
diskInfo,
@@ -13,7 +16,6 @@ export async function GET(request: NextRequest) {
uptimeInfo,
networkInfo
] = await Promise.all([
si.osInfo(),
si.mem(),
si.cpu(),
si.fsSize(),
@@ -46,32 +48,32 @@ export async function GET(request: NextRequest) {
const actualUsed = memInfo.active || memInfo.used;
const actualFree = memInfo.available || memInfo.free;
const memUsage = ((actualUsed / memInfo.total) * 100);
let memStatus = "Optimal";
if (memUsage > 90) memStatus = "Critical";
else if (memUsage > 80) memStatus = "High";
else if (memUsage > 70) memStatus = "Moderate";
let memStatus = t("system.optimal");
if (memUsage > 90) memStatus = t("system.critical");
else if (memUsage > 80) memStatus = t("system.high");
else if (memUsage > 70) memStatus = t("system.moderate");
const rootDisk = diskInfo.find(disk => disk.mount === '/') || diskInfo[0];
const diskUsage = rootDisk ? ((rootDisk.used / rootDisk.size) * 100) : 0;
let diskStatus = "Optimal";
if (diskUsage > 90) diskStatus = "Critical";
else if (diskUsage > 80) diskStatus = "High";
else if (diskUsage > 70) diskStatus = "Moderate";
let diskStatus = t("system.optimal");
if (diskUsage > 90) diskStatus = t("system.critical");
else if (diskUsage > 80) diskStatus = t("system.high");
else if (diskUsage > 70) diskStatus = t("system.moderate");
const cpuStatus = loadInfo.currentLoad > 80 ? "High" :
loadInfo.currentLoad > 60 ? "Moderate" : "Optimal";
const cpuStatus = loadInfo.currentLoad > 80 ? t("system.high") :
loadInfo.currentLoad > 60 ? t("system.moderate") : t("system.optimal");
const criticalThreshold = 90;
const warningThreshold = 80;
let overallStatus = "Optimal";
let statusDetails = "All systems running normally";
let overallStatus = t("system.optimal");
let statusDetails = t("system.allSystemsRunningNormally");
if (memUsage > criticalThreshold || loadInfo.currentLoad > criticalThreshold || diskUsage > criticalThreshold) {
overallStatus = "Critical";
statusDetails = "High resource usage detected - immediate attention required";
overallStatus = t("system.critical");
statusDetails = t("system.highResourceUsageDetectedImmediateAttentionRequired");
} else if (memUsage > warningThreshold || loadInfo.currentLoad > warningThreshold || diskUsage > warningThreshold) {
overallStatus = "Warning";
statusDetails = "Moderate resource usage - monitoring recommended";
overallStatus = t("system.warning");
statusDetails = t("system.moderateResourceUsageMonitoringRecommended");
}
let mainInterface: any = null;
@@ -85,7 +87,7 @@ export async function GET(request: NextRequest) {
const networkSpeed = mainInterface && 'rx_sec' in mainInterface && 'tx_sec' in mainInterface
? `${Math.round(((mainInterface.rx_sec || 0) + (mainInterface.tx_sec || 0)) / 1024 / 1024)} Mbps`
: "Unknown";
: t("system.unknown");
let latency = 0;
try {
@@ -117,9 +119,9 @@ export async function GET(request: NextRequest) {
status: cpuStatus,
},
disk: {
total: rootDisk ? formatBytes(rootDisk.size) : "Unknown",
used: rootDisk ? formatBytes(rootDisk.used) : "Unknown",
free: rootDisk ? formatBytes(rootDisk.available) : "Unknown",
total: rootDisk ? formatBytes(rootDisk.size) : t("system.unknown"),
used: rootDisk ? formatBytes(rootDisk.used) : t("system.unknown"),
free: rootDisk ? formatBytes(rootDisk.available) : t("system.unknown"),
usage: Math.round(diskUsage),
status: diskStatus,
},
@@ -128,7 +130,7 @@ export async function GET(request: NextRequest) {
latency: latency,
downloadSpeed: mainInterface && 'rx_sec' in mainInterface ? Math.round((mainInterface.rx_sec || 0) / 1024 / 1024) : 0,
uploadSpeed: mainInterface && 'tx_sec' in mainInterface ? Math.round((mainInterface.tx_sec || 0) / 1024 / 1024) : 0,
status: mainInterface && 'operstate' in mainInterface && mainInterface.operstate === 'up' ? "Connected" : "Unknown",
status: mainInterface && 'operstate' in mainInterface && mainInterface.operstate === 'up' ? t("system.connected") : t("system.unknown"),
},
systemStatus: {
overall: overallStatus,
@@ -141,20 +143,20 @@ export async function GET(request: NextRequest) {
if (graphics.controllers && graphics.controllers.length > 0) {
const gpu = graphics.controllers[0];
systemStats.gpu = {
model: gpu.model || "Unknown GPU",
model: gpu.model || t("system.unknownGPU"),
memory: gpu.vram ? `${gpu.vram} MB` : undefined,
status: "Available",
status: t("system.available"),
};
} else {
systemStats.gpu = {
model: "No GPU detected",
status: "Unknown",
model: t("system.noGPUDetected"),
status: t("system.unknown"),
};
}
} catch (error) {
systemStats.gpu = {
model: "GPU detection failed",
status: "Unknown",
model: t("system.gpuDetectionFailed"),
status: t("system.unknown"),
};
}

24
app/i18n.ts Normal file
View File

@@ -0,0 +1,24 @@
import { getRequestConfig } from "next-intl/server";
import { Locales } from "@/app/_consts/global";
const validLocales = Locales.map((item) => item.locale);
export default getRequestConfig(async ({ locale }) => {
const safeLocale = locale && validLocales.includes(locale) ? locale : "en";
try {
return {
locale: safeLocale,
messages: (await import(`./_translations/${safeLocale}.json`)).default,
};
} catch (error) {
console.error(
`Failed to load translations for locale: ${safeLocale}`,
error
);
return {
locale: "en",
messages: (await import("./_translations/en.json")).default,
};
}
});

View File

@@ -3,6 +3,9 @@ import { JetBrains_Mono, Inter } from "next/font/google";
import "@/app/globals.css";
import { ThemeProvider } from "@/app/_providers/ThemeProvider";
import { ServiceWorkerRegister } from "@/app/_components/FeatureComponents/PWA/ServiceWorkerRegister";
import { Locales } from "@/app/_consts/global";
import { NextIntlClientProvider } from "next-intl";
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
@@ -43,11 +46,21 @@ export const viewport = {
themeColor: "#3b82f6",
};
export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
let locale = process.env.LOCALE || "en";
let messages;
if (!Locales.some((item) => item.locale === locale)) {
locale = "en";
}
messages = (await import(`./_translations/${locale}.json`)).default;
return (
<html lang="en" suppressHydrationWarning>
<head>
@@ -59,15 +72,18 @@ export default function RootLayout({
<link rel="apple-touch-icon" href="/logo.png" />
</head>
<body className={`${inter.variable} ${jetbrainsMono.variable} font-sans`}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
<ServiceWorkerRegister />
<NextIntlClientProvider locale={locale} messages={messages}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
<ServiceWorkerRegister />
</NextIntlClientProvider>
</body>
</html>
);

View File

@@ -6,9 +6,12 @@ import { ThemeToggle } from "@/app/_components/FeatureComponents/Theme/ThemeTogg
import { LogoutButton } from "@/app/_components/FeatureComponents/LoginForm/LogoutButton";
import { ToastContainer } from "@/app/_components/GlobalComponents/UIElements/Toast";
import { PWAInstallPrompt } from "@/app/_components/FeatureComponents/PWA/PWAInstallPrompt";
import { getTranslations } from "next-intl/server";
export const dynamic = "force-dynamic";
export default async function Home() {
const t = await getTranslations();
const [cronJobs, scripts] = await Promise.all([
getCronJobs(),
fetchScripts(),
@@ -65,7 +68,7 @@ export default async function Home() {
Cr*nMaster
</h1>
<p className="text-xs text-muted-foreground font-mono tracking-wide">
Cron Management made easy
{t("common.cronManagementMadeEasy")}
</p>
</div>
</div>

View File

@@ -1,3 +1,5 @@
const withNextIntl = require('next-intl/plugin')('./app/i18n.ts');
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
@@ -8,6 +10,13 @@ const withPWA = require('next-pwa')({
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
'osx-temperature-sensor': false,
};
return config;
},
async headers() {
return [
{
@@ -27,4 +36,6 @@ const nextConfig = {
},
}
module.exports = withPWA(nextConfig)
module.exports = withNextIntl({
...withPWA(nextConfig)
});

View File

@@ -31,6 +31,7 @@
"lucide-react": "^0.294.0",
"minimatch": "^10.0.3",
"next": "14.0.4",
"next-intl": "^4.4.0",
"next-pwa": "^5.6.0",
"next-themes": "^0.2.1",
"postcss": "^8",

View File

@@ -955,6 +955,54 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
"@formatjs/ecma402-abstract@2.3.6":
version "2.3.6"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz#d6ca9d3579054fe1e1a0a0b5e872e0d64922e4e1"
integrity sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==
dependencies:
"@formatjs/fast-memoize" "2.2.7"
"@formatjs/intl-localematcher" "0.6.2"
decimal.js "^10.4.3"
tslib "^2.8.0"
"@formatjs/fast-memoize@2.2.7", "@formatjs/fast-memoize@^2.2.0":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz#707f9ddaeb522a32f6715bb7950b0831f4cc7b15"
integrity sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==
dependencies:
tslib "^2.8.0"
"@formatjs/icu-messageformat-parser@2.11.4":
version "2.11.4"
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz#63bd2cd82d08ae2bef55adeeb86486df68826f32"
integrity sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==
dependencies:
"@formatjs/ecma402-abstract" "2.3.6"
"@formatjs/icu-skeleton-parser" "1.8.16"
tslib "^2.8.0"
"@formatjs/icu-skeleton-parser@1.8.16":
version "1.8.16"
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz#13f81f6845c7cf6599623006aacaf7d6b4ad2970"
integrity sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==
dependencies:
"@formatjs/ecma402-abstract" "2.3.6"
tslib "^2.8.0"
"@formatjs/intl-localematcher@0.6.2":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz#e9ebe0b4082d7d48e5b2d753579fb7ece4eaefea"
integrity sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==
dependencies:
tslib "^2.8.0"
"@formatjs/intl-localematcher@^0.5.4":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz#1e0bd3fc1332c1fe4540cfa28f07e9227b659a58"
integrity sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==
dependencies:
tslib "2"
"@humanwhocodes/config-array@^0.13.0":
version "0.13.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
@@ -1209,6 +1257,11 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz#326a7b46f6d4cfa54ae25bb888551697873069b4"
integrity sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==
"@schummar/icu-type-parser@1.21.5":
version "1.21.5"
resolved "https://registry.yarnpkg.com/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz#75989085bbbf80ee325874a0137437bde77e9baf"
integrity sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@@ -2110,6 +2163,11 @@ debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4
dependencies:
ms "^2.1.3"
decimal.js@^10.4.3:
version "10.6.0"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
deep-is@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
@@ -3026,6 +3084,16 @@ internal-slot@^1.1.0:
hasown "^2.0.2"
side-channel "^1.1.0"
intl-messageformat@^10.5.14:
version "10.7.18"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.18.tgz#51a6f387afbca9b0f881b2ec081566db8c540b0d"
integrity sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==
dependencies:
"@formatjs/ecma402-abstract" "2.3.6"
"@formatjs/fast-memoize" "2.2.7"
"@formatjs/icu-messageformat-parser" "2.11.4"
tslib "^2.8.0"
is-alphabetical@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
@@ -3673,6 +3741,20 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
negotiator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
next-intl@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/next-intl/-/next-intl-4.4.0.tgz#f8141153ba8029eddf118ad90fe938e08431a5af"
integrity sha512-QHqnP9V9Pe7Tn0PdVQ7u1Z8k9yCkW5SJKeRy2g5gxzhSt/C01y3B9qNxuj3Fsmup/yreIHe6osxU6sFa+9WIkQ==
dependencies:
"@formatjs/intl-localematcher" "^0.5.4"
negotiator "^1.0.0"
use-intl "^4.4.0"
next-pwa@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4"
@@ -4843,7 +4925,7 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@^2.4.0:
tslib@2, tslib@^2.4.0, tslib@^2.8.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@@ -5017,6 +5099,15 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
use-intl@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/use-intl/-/use-intl-4.4.0.tgz#f8d05c3835b71aca1249f34291242b30a9c0c6d0"
integrity sha512-smFekJWtokDRBLC5/ZumlBREzdXOkw06+56Ifj2uRe9266Mk+yWQm2PcJO+EwlOE5sHIXHixOTzN6V8E0RGUbw==
dependencies:
"@formatjs/fast-memoize" "^2.2.0"
"@schummar/icu-type-parser" "1.21.5"
intl-messageformat "^10.5.14"
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"