update prettier
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-23 14:23:46 +01:00
parent 33d1425fbb
commit cbabf43584
25 changed files with 1373 additions and 1363 deletions

View File

@@ -1,28 +1,28 @@
import { useCallback, useEffect, useState } from 'react';
export function useCooldownTimer(seconds = 0, enabled = true) {
const [cooldown, setCooldown] = useState(seconds);
const [cooldown, setCooldown] = useState(seconds);
useEffect(() => {
if (!enabled || cooldown <= 0) {
return;
}
useEffect(() => {
if (!enabled || cooldown <= 0) {
return;
}
const timer = globalThis.setInterval(() => {
setCooldown((prev) => (prev > 0 ? prev - 1 : 0));
}, 1000);
const timer = globalThis.setInterval(() => {
setCooldown((prev) => (prev > 0 ? prev - 1 : 0));
}, 1000);
return () => {
globalThis.clearInterval(timer);
return () => {
globalThis.clearInterval(timer);
};
}, [enabled, cooldown]);
const startCooldown = useCallback((seconds: number) => {
setCooldown(Math.max(0, Math.floor(seconds)));
}, []);
return {
cooldown,
startCooldown,
};
}, [enabled, cooldown]);
const startCooldown = useCallback((seconds: number) => {
setCooldown(Math.max(0, Math.floor(seconds)));
}, []);
return {
cooldown,
startCooldown,
};
}

View File

@@ -4,77 +4,77 @@ import { useValidatedFields } from './useValidatedFields';
type FieldErrors<TValues> = Partial<Record<keyof TValues, string | undefined>>;
type UseEditableFormOptions<TValues> = {
initialValues: TValues;
validate: (values: TValues) => FieldErrors<TValues>;
initialValues: TValues;
validate: (values: TValues) => FieldErrors<TValues>;
};
export function useEditableForm<TValues extends Record<string, string>>({
initialValues,
validate,
}: UseEditableFormOptions<TValues>) {
const [isEditing, setIsEditing] = useState(false);
const {
values,
errors,
isValid,
setValues,
setFieldValue,
validateAll,
setFieldError,
setErrors,
clearErrors,
} = useValidatedFields({
initialValues,
validate,
});
}: UseEditableFormOptions<TValues>) {
const [isEditing, setIsEditing] = useState(false);
const startEditing = useCallback(
(sourceValues: TValues) => {
setValues(sourceValues, { validate: true });
setIsEditing(true);
},
[setValues],
);
const {
values,
errors,
isValid,
setValues,
setFieldValue,
validateAll,
setFieldError,
setErrors,
clearErrors,
} = useValidatedFields({
initialValues,
validate,
});
const discardChanges = useCallback(
(sourceValues: TValues) => {
setValues(sourceValues, { clearErrors: true });
setIsEditing(false);
},
[setValues],
);
const startEditing = useCallback(
(sourceValues: TValues) => {
setValues(sourceValues, { validate: true });
setIsEditing(true);
},
[setValues],
);
const loadFromSource = useCallback(
(sourceValues: TValues) => {
setValues(sourceValues, { clearErrors: true });
},
[setValues],
);
const discardChanges = useCallback(
(sourceValues: TValues) => {
setValues(sourceValues, { clearErrors: true });
setIsEditing(false);
},
[setValues],
);
const commitSaved = useCallback(
(sourceValues: TValues) => {
setValues(sourceValues, { clearErrors: true });
setIsEditing(false);
},
[setValues],
);
const loadFromSource = useCallback(
(sourceValues: TValues) => {
setValues(sourceValues, { clearErrors: true });
},
[setValues],
);
return {
values,
errors,
isValid,
setValues,
setFieldValue,
validateAll,
setFieldError,
setErrors,
clearErrors,
isEditing,
startEditing,
discardChanges,
loadFromSource,
commitSaved,
setIsEditing,
};
const commitSaved = useCallback(
(sourceValues: TValues) => {
setValues(sourceValues, { clearErrors: true });
setIsEditing(false);
},
[setValues],
);
return {
values,
errors,
isValid,
setValues,
setFieldValue,
validateAll,
setFieldError,
setErrors,
clearErrors,
isEditing,
startEditing,
discardChanges,
loadFromSource,
commitSaved,
setIsEditing,
};
}

View File

@@ -1,107 +1,111 @@
import { useCallback, useEffect, useState } from 'react';
type PaginatedResourceResponse<TItem> = {
items: TItem[];
page: number;
pageSize: number;
total: number;
totalPages: number;
items: TItem[];
page: number;
pageSize: number;
total: number;
totalPages: number;
};
type UsePaginatedResourceOptions<TItem> = {
load: (params: {
q: string;
page: number;
pageSize: number;
load: (params: {
q: string;
page: number;
pageSize: number;
sort?: string;
}) => Promise<PaginatedResourceResponse<TItem>>;
sort?: string;
}) => Promise<PaginatedResourceResponse<TItem>>;
sort?: string;
debounceMs?: number;
initialQuery?: string;
initialPage?: number;
initialPageSize?: number;
debounceMs?: number;
initialQuery?: string;
initialPage?: number;
initialPageSize?: number;
};
export function usePaginatedResource<TItem>({
load,
sort,
debounceMs = 250,
initialQuery = '',
initialPage = 1,
initialPageSize = 10,
load,
sort,
debounceMs = 250,
initialQuery = '',
initialPage = 1,
initialPageSize = 10,
}: UsePaginatedResourceOptions<TItem>) {
const [items, setItems] = useState<TItem[]>([]);
const [q, setQ] = useState(initialQuery);
const [page, setPage] = useState(initialPage);
const [pageSize, setPageSize] = useState(initialPageSize);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [items, setItems] = useState<TItem[]>([]);
const [q, setQ] = useState(initialQuery);
const [page, setPage] = useState(initialPage);
const [pageSize, setPageSize] = useState(initialPageSize);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setIsLoading(true);
setError(null);
useEffect(() => {
let cancelled = false;
setIsLoading(true);
setError(null);
const timer = setTimeout(() => {
void (async () => {
try {
const response = await load({
q,
page,
pageSize,
sort,
});
const timer = setTimeout(() => {
void (async () => {
try {
const response = await load({
q,
page,
pageSize,
sort,
});
if (cancelled) {
return;
}
if (cancelled) {
return;
}
setItems(response.items);
setTotal(response.total);
setTotalPages(response.totalPages);
setPage(response.page);
setPageSize(response.pageSize);
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : 'Request failed. Please try again.');
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
})();
}, debounceMs);
setItems(response.items);
setTotal(response.total);
setTotalPages(response.totalPages);
setPage(response.page);
setPageSize(response.pageSize);
} catch (err) {
if (!cancelled) {
setError(
err instanceof Error
? err.message
: 'Request failed. Please try again.',
);
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
})();
}, debounceMs);
return () => {
cancelled = true;
clearTimeout(timer);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [q, page, pageSize, sort, load, debounceMs]);
const setQuery = useCallback((value: string) => {
setQ(value);
setPage(1);
}, []);
const setPageSizeAndResetPage = useCallback((value: number) => {
setPageSize(value);
setPage(1);
}, []);
return {
items,
q,
page,
pageSize,
total,
totalPages,
error,
isLoading,
setQuery,
setPage,
setPageSize: setPageSizeAndResetPage,
};
}, [q, page, pageSize, sort, load, debounceMs]);
const setQuery = useCallback((value: string) => {
setQ(value);
setPage(1);
}, []);
const setPageSizeAndResetPage = useCallback((value: number) => {
setPageSize(value);
setPage(1);
}, []);
return {
items,
q,
page,
pageSize,
total,
totalPages,
error,
isLoading,
setQuery,
setPage,
setPageSize: setPageSizeAndResetPage,
};
}

View File

@@ -3,79 +3,79 @@ import { useCallback, useMemo, useState } from 'react';
export type SortDirection = 'asc' | 'desc';
export type SortState = {
field: string;
direction: SortDirection;
field: string;
direction: SortDirection;
};
function invertDirection(direction: SortDirection): SortDirection {
return direction === 'asc' ? 'desc' : 'asc';
return direction === 'asc' ? 'desc' : 'asc';
}
export function formatSortParam(sort: SortState | null | undefined): string | undefined {
if (!sort) {
return undefined;
}
return sort.direction === 'desc' ? `-${sort.field}` : sort.field;
if (!sort) {
return undefined;
}
return sort.direction === 'desc' ? `-${sort.field}` : sort.field;
}
type UseSortingResult = {
activeSort: SortState | null;
sortParam: string | undefined;
toggleSort: (field: string) => void;
setSort: (next: SortState | null) => void;
resetSort: () => void;
activeSort: SortState | null;
sortParam: string | undefined;
toggleSort: (field: string) => void;
setSort: (next: SortState | null) => void;
resetSort: () => void;
};
export function useSorting(defaultSort?: SortState | null): UseSortingResult {
const [overrideSort, setOverrideSort] = useState<SortState | null>(null);
const [overrideSort, setOverrideSort] = useState<SortState | null>(null);
const activeSort = overrideSort ?? defaultSort ?? null;
const activeSort = overrideSort ?? defaultSort ?? null;
const toggleSort = useCallback(
(field: string) => {
setOverrideSort((previousOverride) => {
const baselineSort = defaultSort ?? null;
const currentSort = previousOverride ?? baselineSort;
const toggleSort = useCallback(
(field: string) => {
setOverrideSort((previousOverride) => {
const baselineSort = defaultSort ?? null;
const currentSort = previousOverride ?? baselineSort;
if (!currentSort || currentSort.field !== field) {
return { field, direction: 'asc' };
}
if (!currentSort || currentSort.field !== field) {
return { field, direction: 'asc' };
}
if (baselineSort && baselineSort.field === field) {
if (previousOverride == null) {
return { field, direction: invertDirection(baselineSort.direction) };
}
if (previousOverride.direction === baselineSort.direction) {
return { field, direction: invertDirection(baselineSort.direction) };
}
return null;
}
if (baselineSort && baselineSort.field === field) {
if (previousOverride == null) {
return { field, direction: invertDirection(baselineSort.direction) };
}
if (previousOverride.direction === baselineSort.direction) {
return { field, direction: invertDirection(baselineSort.direction) };
}
return null;
}
if (previousOverride == null || previousOverride.direction === 'desc') {
return null;
}
if (previousOverride == null || previousOverride.direction === 'desc') {
return null;
}
return { field, direction: 'desc' };
});
},
[defaultSort],
);
return { field, direction: 'desc' };
});
},
[defaultSort],
);
const setSort = useCallback((next: SortState | null) => {
setOverrideSort(next);
}, []);
const setSort = useCallback((next: SortState | null) => {
setOverrideSort(next);
}, []);
const resetSort = useCallback(() => {
setOverrideSort(null);
}, []);
const resetSort = useCallback(() => {
setOverrideSort(null);
}, []);
const sortParam = useMemo(() => formatSortParam(activeSort), [activeSort]);
const sortParam = useMemo(() => formatSortParam(activeSort), [activeSort]);
return {
activeSort,
sortParam,
toggleSort,
setSort,
resetSort,
};
return {
activeSort,
sortParam,
toggleSort,
setSort,
resetSort,
};
}

View File

@@ -1,31 +1,31 @@
import { useCallback, useState } from 'react';
export function useSubmitState<TStatus = string | null>(initialStatus: TStatus) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [status, setStatus] = useState<TStatus>(initialStatus);
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [status, setStatus] = useState<TStatus>(initialStatus);
const startSubmitting = useCallback(() => {
setIsSubmitting(true);
}, []);
const startSubmitting = useCallback(() => {
setIsSubmitting(true);
}, []);
const finishSubmitting = useCallback(() => {
setIsSubmitting(false);
}, []);
const finishSubmitting = useCallback(() => {
setIsSubmitting(false);
}, []);
const clearFeedback = useCallback(() => {
setSubmitError(null);
setStatus(initialStatus);
}, [initialStatus]);
const clearFeedback = useCallback(() => {
setSubmitError(null);
setStatus(initialStatus);
}, [initialStatus]);
return {
isSubmitting,
submitError,
status,
startSubmitting,
finishSubmitting,
setSubmitError,
setStatus,
clearFeedback,
};
return {
isSubmitting,
submitError,
status,
startSubmitting,
finishSubmitting,
setSubmitError,
setStatus,
clearFeedback,
};
}

View File

@@ -4,168 +4,172 @@ type FieldErrors<TValues> = Partial<Record<keyof TValues, string | undefined>>;
type TouchedFields<TValues> = Partial<Record<keyof TValues, boolean>>;
type SetValuesOptions = {
validate?: boolean;
clearErrors?: boolean;
validate?: boolean;
clearErrors?: boolean;
};
type SetFieldValueOptions = {
validate?: boolean;
touch?: boolean;
validate?: boolean;
touch?: boolean;
};
type ValidateAllOptions = {
touchAll?: boolean;
touchAll?: boolean;
};
type UseValidatedFieldsOptions<TValues> = {
initialValues: TValues;
validate: (values: TValues) => FieldErrors<TValues>;
initialValues: TValues;
validate: (values: TValues) => FieldErrors<TValues>;
};
function hasErrors<TValues>(errors: FieldErrors<TValues>): boolean {
return Object.values(errors).some(Boolean);
return Object.values(errors).some(Boolean);
}
function pickTouchedErrors<TValues>(
errors: FieldErrors<TValues>,
touched: TouchedFields<TValues>,
errors: FieldErrors<TValues>,
touched: TouchedFields<TValues>,
): FieldErrors<TValues> {
const next: FieldErrors<TValues> = {};
const next: FieldErrors<TValues> = {};
for (const key of Object.keys(errors) as Array<keyof TValues>) {
if (touched[key]) {
next[key] = errors[key];
for (const key of Object.keys(errors) as Array<keyof TValues>) {
if (touched[key]) {
next[key] = errors[key];
}
}
}
return next;
return next;
}
function touchAll<TValues extends Record<string, string>>(values: TValues): TouchedFields<TValues> {
const touched: TouchedFields<TValues> = {};
const touched: TouchedFields<TValues> = {};
for (const key of Object.keys(values) as Array<keyof TValues>) {
touched[key] = true;
}
for (const key of Object.keys(values) as Array<keyof TValues>) {
touched[key] = true;
}
return touched;
return touched;
}
export function useValidatedFields<TValues extends Record<string, string>>({
initialValues,
validate,
initialValues,
validate,
}: UseValidatedFieldsOptions<TValues>) {
const [values, setValues] = useState<TValues>(initialValues);
const [allErrors, setAllErrors] = useState<FieldErrors<TValues>>(() => validate(initialValues));
const [touched, setTouched] = useState<TouchedFields<TValues>>({});
const [values, setValues] = useState<TValues>(initialValues);
const [allErrors, setAllErrors] = useState<FieldErrors<TValues>>(() => validate(initialValues));
const [touched, setTouched] = useState<TouchedFields<TValues>>({});
const updateValues = useCallback(
(nextValues: TValues, options: SetValuesOptions = {}) => {
const { validate: shouldValidate = false, clearErrors = false } = options;
setValues(nextValues);
const updateValues = useCallback(
(nextValues: TValues, options: SetValuesOptions = {}) => {
const { validate: shouldValidate = false, clearErrors = false } = options;
setValues(nextValues);
if (shouldValidate || clearErrors) {
setAllErrors(validate(nextValues));
}
if (shouldValidate || clearErrors) {
setAllErrors(validate(nextValues));
}
if (clearErrors) {
setTouched({});
}
},
[validate],
);
if (clearErrors) {
setTouched({});
}
},
[validate],
);
const setFieldValue = useCallback(
<K extends keyof TValues>(key: K, value: TValues[K], options: SetFieldValueOptions = {}) => {
const { validate: shouldValidate = true, touch = true } = options;
const setFieldValue = useCallback(
<K extends keyof TValues>(
key: K,
value: TValues[K],
options: SetFieldValueOptions = {},
) => {
const { validate: shouldValidate = true, touch = true } = options;
if (touch) {
if (touch) {
setTouched((current) => ({
...current,
[key]: true,
}));
}
setValues((current) => {
const nextValues = {
...current,
[key]: value,
};
if (shouldValidate) {
setAllErrors(validate(nextValues));
}
return nextValues;
});
},
[validate],
);
const validateAll = useCallback(
(options: ValidateAllOptions = {}) => {
const { touchAll: shouldTouchAll = true } = options;
const nextErrors = validate(values);
setAllErrors(nextErrors);
if (shouldTouchAll) {
setTouched(touchAll(values));
}
return nextErrors;
},
[validate, values],
);
const setFieldError = useCallback(<K extends keyof TValues>(key: K, message?: string) => {
setTouched((current) => ({
...current,
[key]: true,
...current,
[key]: true,
}));
}
setValues((current) => {
const nextValues = {
...current,
[key]: value,
};
setAllErrors((current) => ({
...current,
[key]: message,
}));
}, []);
if (shouldValidate) {
setAllErrors(validate(nextValues));
const updateErrors = useCallback((nextErrors: FieldErrors<TValues>) => {
const nextTouched: TouchedFields<TValues> = {};
for (const key of Object.keys(nextErrors) as Array<keyof TValues>) {
if (nextErrors[key]) {
nextTouched[key] = true;
}
}
return nextValues;
});
},
[validate],
);
setTouched((current) => ({
...current,
...nextTouched,
}));
setAllErrors(nextErrors);
}, []);
const validateAll = useCallback(
(options: ValidateAllOptions = {}) => {
const { touchAll: shouldTouchAll = true } = options;
const nextErrors = validate(values);
const clearErrors = useCallback(() => {
setAllErrors(validate(values));
setTouched({});
}, [validate, values]);
setAllErrors(nextErrors);
const errors = useMemo(() => pickTouchedErrors(allErrors, touched), [allErrors, touched]);
if (shouldTouchAll) {
setTouched(touchAll(values));
}
const isValid = useMemo(() => {
return !hasErrors(validate(values));
}, [validate, values]);
return nextErrors;
},
[validate, values],
);
const setFieldError = useCallback(<K extends keyof TValues>(key: K, message?: string) => {
setTouched((current) => ({
...current,
[key]: true,
}));
setAllErrors((current) => ({
...current,
[key]: message,
}));
}, []);
const updateErrors = useCallback((nextErrors: FieldErrors<TValues>) => {
const nextTouched: TouchedFields<TValues> = {};
for (const key of Object.keys(nextErrors) as Array<keyof TValues>) {
if (nextErrors[key]) {
nextTouched[key] = true;
}
}
setTouched((current) => ({
...current,
...nextTouched,
}));
setAllErrors(nextErrors);
}, []);
const clearErrors = useCallback(() => {
setAllErrors(validate(values));
setTouched({});
}, [validate, values]);
const errors = useMemo(() => pickTouchedErrors(allErrors, touched), [allErrors, touched]);
const isValid = useMemo(() => {
return !hasErrors(validate(values));
}, [validate, values]);
return {
values,
errors,
isValid,
setValues: updateValues,
setFieldValue,
validateAll,
setFieldError,
setErrors: updateErrors,
clearErrors,
};
return {
values,
errors,
isValid,
setValues: updateValues,
setFieldValue,
validateAll,
setFieldError,
setErrors: updateErrors,
clearErrors,
};
}