refactor(auth): replace escaped persian digits
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { Banknote, CalendarDays, Check, ChevronDown, Clock3, DollarSign, MoreVertical, Pencil, Play, Plus, Search, Square, Tag as TagIcon, Trash2 } from "lucide-react";
|
import { Banknote, CalendarDays, Check, ChevronDown, Clock3, DollarSign, MoreVertical, Pencil, Play, Plus, Search, Square, Tag as TagIcon, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { getProjects, type Project } from "../api/projects";
|
import { getProjects, type Project } from "../api/projects";
|
||||||
import { getMyWorkspaceRates, type MyWorkspaceRatesResponse } from "../api/rates";
|
import { getMyWorkspaceRates, type MyWorkspaceRatesResponse } from "../api/rates";
|
||||||
import {
|
import {
|
||||||
createTimeEntry,
|
createTimeEntry,
|
||||||
deleteTimeEntry,
|
deleteTimeEntry,
|
||||||
getTimeEntries,
|
getTimeEntries,
|
||||||
@@ -19,10 +19,10 @@ import {
|
|||||||
} from "../api/timeEntries";
|
} from "../api/timeEntries";
|
||||||
import { getTags, type Tag } from "../api/tags";
|
import { getTags, type Tag } from "../api/tags";
|
||||||
import { Modal } from "../components/Modal";
|
import { Modal } from "../components/Modal";
|
||||||
import EmptyStateCard from "../components/EmptyStateCard";
|
import EmptyStateCard from "../components/EmptyStateCard";
|
||||||
import { InfiniteScroll } from "../components/InfiniteScroll";
|
import { InfiniteScroll } from "../components/InfiniteScroll";
|
||||||
import { WorkspaceRatesPanel } from "../components/rates/WorkspaceRatesPanel";
|
import { WorkspaceRatesPanel } from "../components/rates/WorkspaceRatesPanel";
|
||||||
import TimesheetFilterBar, { type TimeEntryFilters } from "../components/timesheet/TimesheetFilterBar";
|
import TimesheetFilterBar, { type TimeEntryFilters } from "../components/timesheet/TimesheetFilterBar";
|
||||||
import JalaliDatePicker from "../components/ui/JalaliDatePicker";
|
import JalaliDatePicker from "../components/ui/JalaliDatePicker";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
import { Input } from "../components/ui/input";
|
import { Input } from "../components/ui/input";
|
||||||
@@ -1262,10 +1262,10 @@ function EntryEditorFields({
|
|||||||
<div className="grid min-w-0 flex-1 grid-cols-[minmax(430px,1fr)_minmax(0,220px)_40px_188px_40px] 2xl:grid-cols-[minmax(430px,1fr)_minmax(0,max-content)_40px_188px_40px] items-center">
|
<div className="grid min-w-0 flex-1 grid-cols-[minmax(430px,1fr)_minmax(0,220px)_40px_188px_40px] 2xl:grid-cols-[minmax(430px,1fr)_minmax(0,max-content)_40px_188px_40px] items-center">
|
||||||
<div className="flex min-w-0 items-center">
|
<div className="flex min-w-0 items-center">
|
||||||
<Input
|
<Input
|
||||||
value={state.description}
|
value={state.description}
|
||||||
onChange={(event) => onChange({ description: event.target.value })}
|
onChange={(event) => onChange({ description: event.target.value })}
|
||||||
placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
|
placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
|
||||||
className="h-12 w-[200px] 2xl:w-[400px] shrink-0 rounded-none border-0 bg-transparent px-0 pe-3 text-sm shadow-none placeholder:text-slate-300 focus-visible:ring-0 focus-visible:ring-offset-0 truncate dark:bg-transparent dark:text-slate-100 dark:placeholder:text-slate-600"
|
className="h-12 w-[200px] 2xl:w-[400px] shrink-0 rounded-none border-0 bg-transparent px-0 pe-3 text-sm shadow-none placeholder:text-slate-300 focus-visible:ring-0 focus-visible:ring-offset-0 truncate dark:bg-transparent dark:text-slate-100 dark:placeholder:text-slate-600"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="me-2 shrink-0 text-sm font-semibold leading-none text-sky-600 dark:text-sky-400">•</span>
|
<span className="me-2 shrink-0 text-sm font-semibold leading-none text-sky-600 dark:text-sky-400">•</span>
|
||||||
@@ -1344,11 +1344,11 @@ function EntryEditorFields({
|
|||||||
<label className={`block font-medium text-slate-700 dark:text-slate-300 ${compact ? "mb-0.5 text-[11px]" : "mb-1 text-sm"}`}>
|
<label className={`block font-medium text-slate-700 dark:text-slate-300 ${compact ? "mb-0.5 text-[11px]" : "mb-1 text-sm"}`}>
|
||||||
{t.timesheet?.descriptionLabel || "Description"}
|
{t.timesheet?.descriptionLabel || "Description"}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
value={state.description}
|
value={state.description}
|
||||||
onChange={(event) => onChange({ description: event.target.value })}
|
onChange={(event) => onChange({ description: event.target.value })}
|
||||||
placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
|
placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
|
||||||
className={compact ? "h-9 px-2 text-xs placeholder:text-slate-300 dark:placeholder:text-slate-600" : "placeholder:text-slate-300 dark:placeholder:text-slate-600"}
|
className={compact ? "h-9 px-2 text-xs placeholder:text-slate-300 dark:placeholder:text-slate-600" : "placeholder:text-slate-300 dark:placeholder:text-slate-600"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1837,7 +1837,7 @@ function MobileRecordedEntryCard({
|
|||||||
</p>
|
</p>
|
||||||
{project && (
|
{project && (
|
||||||
<span className="inline-flex min-w-0 items-center gap-2 text-xs">
|
<span className="inline-flex min-w-0 items-center gap-2 text-xs">
|
||||||
<span className="shrink-0 text-sky-600 dark:text-sky-400">{"\u2022"}</span>
|
<span className="shrink-0 text-sky-600 dark:text-sky-400">{"•"}</span>
|
||||||
<span className={`max-w-[10rem] truncate font-medium ${project.isDeleted ? "italic text-slate-500 dark:text-slate-400" : "text-sky-600 dark:text-sky-400"}`}>
|
<span className={`max-w-[10rem] truncate font-medium ${project.isDeleted ? "italic text-slate-500 dark:text-slate-400" : "text-sky-600 dark:text-sky-400"}`}>
|
||||||
{project.isDeleted ? buildDeletedProjectLabel(project.name, deletedProjectLabel) : project.name}
|
{project.isDeleted ? buildDeletedProjectLabel(project.name, deletedProjectLabel) : project.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -2017,13 +2017,13 @@ function TimesheetSkeleton({ loadingLabel }: { loadingLabel: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Timesheet() {
|
export default function Timesheet() {
|
||||||
const { t, lang } = useTranslation();
|
const { t, lang } = useTranslation();
|
||||||
const { activeWorkspace } = useWorkspace();
|
const { activeWorkspace } = useWorkspace();
|
||||||
const [isRatesPanelOpen, setIsRatesPanelOpen] = useState(false);
|
const [isRatesPanelOpen, setIsRatesPanelOpen] = useState(false);
|
||||||
const [isRatesPanelLoading, setIsRatesPanelLoading] = useState(false);
|
const [isRatesPanelLoading, setIsRatesPanelLoading] = useState(false);
|
||||||
const [myRates, setMyRates] = useState<MyWorkspaceRatesResponse | null>(null);
|
const [myRates, setMyRates] = useState<MyWorkspaceRatesResponse | null>(null);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const isRtl = lang === "fa";
|
const isRtl = lang === "fa";
|
||||||
const extendedTimesheet = (t.timesheet as {
|
const extendedTimesheet = (t.timesheet as {
|
||||||
deleteTitle?: string;
|
deleteTitle?: string;
|
||||||
@@ -2163,17 +2163,17 @@ export default function Timesheet() {
|
|||||||
void loadOptions();
|
void loadOptions();
|
||||||
}, [activeWorkspace?.id, t.timesheet?.optionsError]);
|
}, [activeWorkspace?.id, t.timesheet?.optionsError]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGroupedHistory([]);
|
setGroupedHistory([]);
|
||||||
setNextOffset(0);
|
setNextOffset(0);
|
||||||
setHasMoreHistory(false);
|
setHasMoreHistory(false);
|
||||||
}, [activeWorkspace?.id]);
|
}, [activeWorkspace?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsRatesPanelOpen(false);
|
setIsRatesPanelOpen(false);
|
||||||
setIsRatesPanelLoading(false);
|
setIsRatesPanelLoading(false);
|
||||||
setMyRates(null);
|
setMyRates(null);
|
||||||
}, [activeWorkspace?.id]);
|
}, [activeWorkspace?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeoutId = window.setTimeout(() => {
|
const timeoutId = window.setTimeout(() => {
|
||||||
@@ -2606,29 +2606,29 @@ export default function Timesheet() {
|
|||||||
setDebouncedSearchQuery("");
|
setDebouncedSearchQuery("");
|
||||||
}, [setSearchParams]);
|
}, [setSearchParams]);
|
||||||
|
|
||||||
const handleLoadMore = useCallback(() => {
|
const handleLoadMore = useCallback(() => {
|
||||||
if (!hasMoreHistory || nextOffset === null || isLoadingMore) return;
|
if (!hasMoreHistory || nextOffset === null || isLoadingMore) return;
|
||||||
void loadHistory({ offset: nextOffset, append: true });
|
void loadHistory({ offset: nextOffset, append: true });
|
||||||
}, [hasMoreHistory, isLoadingMore, loadHistory, nextOffset]);
|
}, [hasMoreHistory, isLoadingMore, loadHistory, nextOffset]);
|
||||||
|
|
||||||
const openRatesPanel = useCallback(async () => {
|
const openRatesPanel = useCallback(async () => {
|
||||||
if (!activeWorkspace?.id) return;
|
if (!activeWorkspace?.id) return;
|
||||||
|
|
||||||
setIsRatesPanelOpen(true);
|
setIsRatesPanelOpen(true);
|
||||||
if (myRates || isRatesPanelLoading) return;
|
if (myRates || isRatesPanelLoading) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsRatesPanelLoading(true);
|
setIsRatesPanelLoading(true);
|
||||||
const response = await getMyWorkspaceRates(activeWorkspace.id);
|
const response = await getMyWorkspaceRates(activeWorkspace.id);
|
||||||
setMyRates(response);
|
setMyRates(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(t.rates?.projectSaveError || "Failed to load rates.");
|
toast.error(t.rates?.projectSaveError || "Failed to load rates.");
|
||||||
setIsRatesPanelOpen(false);
|
setIsRatesPanelOpen(false);
|
||||||
} finally {
|
} finally {
|
||||||
setIsRatesPanelLoading(false);
|
setIsRatesPanelLoading(false);
|
||||||
}
|
}
|
||||||
}, [activeWorkspace?.id, isRatesPanelLoading, myRates, t.rates?.projectSaveError]);
|
}, [activeWorkspace?.id, isRatesPanelLoading, myRates, t.rates?.projectSaveError]);
|
||||||
|
|
||||||
const handleDiscardTimerDraft = useCallback(async () => {
|
const handleDiscardTimerDraft = useCallback(async () => {
|
||||||
if (!discardTimerModal.entry || isDiscardingTimer) return;
|
if (!discardTimerModal.entry || isDiscardingTimer) return;
|
||||||
@@ -2658,16 +2658,16 @@ export default function Timesheet() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[calc(100vh-73px)] flex-col bg-slate-100/70 p-4 dark:bg-slate-900">
|
<div className="flex min-h-[calc(100vh-73px)] flex-col bg-slate-100/70 p-4 dark:bg-slate-900">
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">{t.timesheet?.title || 'Timesheets'}</h1>
|
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">{t.timesheet?.title || 'Timesheets'}</h1>
|
||||||
<p className="text-slate-500 dark:text-slate-400 mt-1">{t.timesheet?.description(activeWorkspace?.name || "-") || 'Manage your Timesheet'}</p>
|
<p className="text-slate-500 dark:text-slate-400 mt-1">{t.timesheet?.description(activeWorkspace?.name || "-") || 'Manage your Timesheet'}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="secondary" onClick={() => void openRatesPanel()} className="gap-2">
|
<Button type="button" variant="secondary" onClick={() => void openRatesPanel()} className="gap-2">
|
||||||
<Banknote className="h-4 w-4" />
|
<Banknote className="h-4 w-4" />
|
||||||
{t.rates?.myRatesTitle || "My rates"}
|
{t.rates?.myRatesTitle || "My rates"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={desktopTimerRef}
|
ref={desktopTimerRef}
|
||||||
@@ -2677,11 +2677,11 @@ export default function Timesheet() {
|
|||||||
<div className="flex min-w-0 items-center gap-2 px-3 py-3">
|
<div className="flex min-w-0 items-center gap-2 px-3 py-3">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<Input
|
<Input
|
||||||
value={timerDraft.description}
|
value={timerDraft.description}
|
||||||
placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
|
placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
|
||||||
onChange={(event) => setTimerDraft((current) => ({ ...current, description: event.target.value }))}
|
onChange={(event) => setTimerDraft((current) => ({ ...current, description: event.target.value }))}
|
||||||
disabled={isStartingTimer}
|
disabled={isStartingTimer}
|
||||||
className="h-12 rounded-none border-0 bg-transparent px-5 text-sm shadow-none placeholder:text-slate-300 focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-transparent dark:placeholder:text-slate-600"
|
className="h-12 rounded-none border-0 bg-transparent px-5 text-sm shadow-none placeholder:text-slate-300 focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-transparent dark:placeholder:text-slate-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2785,11 +2785,11 @@ export default function Timesheet() {
|
|||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Input
|
<Input
|
||||||
value={timerDraft.description}
|
value={timerDraft.description}
|
||||||
placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
|
placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
|
||||||
onChange={(event) => setTimerDraft((current) => ({ ...current, description: event.target.value }))}
|
onChange={(event) => setTimerDraft((current) => ({ ...current, description: event.target.value }))}
|
||||||
disabled={isStartingTimer}
|
disabled={isStartingTimer}
|
||||||
className="h-10 border-slate-200 bg-slate-50 text-sm placeholder:text-slate-300 dark:border-slate-700 dark:bg-slate-900 dark:placeholder:text-slate-600"
|
className="h-10 border-slate-200 bg-slate-50 text-sm placeholder:text-slate-300 dark:border-slate-700 dark:bg-slate-900 dark:placeholder:text-slate-600"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
|
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
|
||||||
@@ -3100,8 +3100,8 @@ export default function Timesheet() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{discardTimerModal.entry && (
|
{discardTimerModal.entry && (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={discardTimerModal.isOpen}
|
isOpen={discardTimerModal.isOpen}
|
||||||
onClose={closeDiscardTimerModal}
|
onClose={closeDiscardTimerModal}
|
||||||
title={(t.actions as { discard?: string } | undefined)?.discard || "Discard"}
|
title={(t.actions as { discard?: string } | undefined)?.discard || "Discard"}
|
||||||
@@ -3130,15 +3130,15 @@ export default function Timesheet() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<WorkspaceRatesPanel
|
<WorkspaceRatesPanel
|
||||||
open={isRatesPanelOpen}
|
open={isRatesPanelOpen}
|
||||||
data={myRates}
|
data={myRates}
|
||||||
isLoading={isRatesPanelLoading}
|
isLoading={isRatesPanelLoading}
|
||||||
onClose={() => setIsRatesPanelOpen(false)}
|
onClose={() => setIsRatesPanelOpen(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { toast } from "sonner"
|
|||||||
import { ApiError } from "../../api/client"
|
import { ApiError } from "../../api/client"
|
||||||
import { setSessionTokens } from "../../lib/session"
|
import { setSessionTokens } from "../../lib/session"
|
||||||
|
|
||||||
const PERSIAN_DIGITS = ["\u06f0", "\u06f1", "\u06f2", "\u06f3", "\u06f4", "\u06f5", "\u06f6", "\u06f7", "\u06f8", "\u06f9"]
|
const PERSIAN_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"]
|
||||||
|
|
||||||
export const localizeDigits = (value: string, isRtl: boolean) =>
|
export const localizeDigits = (value: string, isRtl: boolean) =>
|
||||||
isRtl ? value.replace(/\d/g, (digit) => PERSIAN_DIGITS[Number.parseInt(digit, 10)] ?? digit) : value
|
isRtl ? value.replace(/\d/g, (digit) => PERSIAN_DIGITS[Number.parseInt(digit, 10)] ?? digit) : value
|
||||||
|
|||||||
Reference in New Issue
Block a user