Vlad Chikalkin 10b36978fe
Feature/10 contacts (#16)
* apps/bot: add feature add contact

* apps/bot: check role 'master' before add contact

* apps/bot: rename createCustomer -> createUser

* remove ';'

* app/bot: add contact define name & phone

* apps/bot: check user already exists w/o telegramId (invited)

* Чтобы добавить контакт, сначала поделитесь своим номером телефона.

* apps/bot: create or update functions

* apps/bot: remove api.ts -> move getCustomer to packages/graphql/api

* packages/graphql: add api/customer tests

* tests for createOrUpdateClient

* fix(apps/web): user is undefined

* fix(apps/web): actions getCustomer

* feat(apps/web): update user photo on app launch

* rename page 'masters' -> 'contacts'

* feat(apps/web): add basic /contacts page

* fix app layout

* refactor customer queries

* add action getProfile

* get customer contacts

* use zustand for contacts

* add loading spinner

* rename filteredContacts -> contacts

* replace zustand with @tanstack/react-query

* profile: use react-query

* refactor updateRole function

* move updateRole closer to profile-card

* beautify actions

* add page 'profile/[telegramId]'

* profile: add button "message to telegram"

* profile: add call feature

* app/bot: normalize phone before register

* do not open keyboard on page load

* contacts: loading spinner

* telegram login: customer.active=true

* update name on telegram first login
2025-01-20 18:11:33 +03:00

96 lines
2.5 KiB
TypeScript

/* eslint-disable promise/prefer-await-to-then */
'use client';
import { type CustomerInput } from '@repo/graphql/types';
import { Input } from '@repo/ui/components/ui/input';
import { Label } from '@repo/ui/components/ui/label';
import { type ChangeEvent, useEffect, useRef, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
type ProfileFieldProps = {
readonly disabled?: boolean;
readonly fieldName?: keyof CustomerInput;
readonly id: string;
readonly label: string;
readonly onChange?: (value: CustomerInput) => Promise<void> | void;
readonly readOnly?: boolean;
readonly value: string;
};
export function ProfileField({
disabled = false,
fieldName,
id,
label,
onChange,
readOnly,
value: initialValue,
}: ProfileFieldProps) {
const [value, setValue] = useState(initialValue);
const { debouncedCallback, isPending } = useDebouncedOnChangeCallback(onChange, fieldName);
const inputRef = useFocus(isPending);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
setValue(newValue);
debouncedCallback(newValue);
};
return (
<div className="space-y-2">
<Label htmlFor={id}>{label}</Label>
<Input
className="bg-secondary outline-none focus:ring-0 focus:ring-offset-0"
disabled={disabled || isPending}
id={id}
onChange={handleChange}
readOnly={readOnly}
ref={inputRef}
value={value}
/>
</div>
);
}
function useDebouncedOnChangeCallback(
callback: ((value: CustomerInput) => Promise<void> | void) | undefined,
fieldName: string | undefined,
) {
const [isPending, setIsPending] = useState(false);
const debouncedCallback = useDebouncedCallback((newValue: string) => {
if (!callback || !fieldName) return;
setIsPending(true);
const result = callback({ [fieldName]: newValue });
if (result instanceof Promise) {
result.finally(() => setIsPending(false));
} else {
setIsPending(false);
}
}, 300);
return {
debouncedCallback,
isPending,
};
}
function useFocus(isPending: boolean) {
const inputRef = useRef<HTMLInputElement | null>(null);
const [isInitialRender, setIsInitialRender] = useState(true);
useEffect(() => {
if (isInitialRender) {
setIsInitialRender(false);
return;
}
if (inputRef.current && isPending) {
inputRef.current.focus();
}
}, [isInitialRender, isPending]);
return inputRef;
}