diff --git a/apps/web/actions/profile.ts b/apps/web/actions/profile.ts
index ca7dc2b..4d9cd4f 100644
--- a/apps/web/actions/profile.ts
+++ b/apps/web/actions/profile.ts
@@ -1,11 +1,28 @@
'use server';
-
+import { authOptions } from '@/config/auth';
+import { getCustomer, updateCustomerProfile } from '@repo/graphql/api';
+import { type CustomerInput } from '@repo/graphql/types';
+import { getServerSession } from 'next-auth/next';
import { revalidatePath } from 'next/cache';
export async function becomeMaster() {
revalidatePath('/profile');
}
-export async function updateName() {
+export async function updateProfile(input: CustomerInput) {
+ const session = await getServerSession(authOptions);
+
+ if (session) {
+ const { user } = session;
+ const getCustomerResponse = await getCustomer({ telegramId: user?.telegramId });
+ const customer = getCustomerResponse.data.customers.at(0);
+ if (customer) {
+ await updateCustomerProfile({
+ data: input,
+ documentId: customer.documentId,
+ });
+ }
+ }
+
revalidatePath('/profile');
}
diff --git a/apps/web/app/(main)/profile/page.tsx b/apps/web/app/(main)/profile/page.tsx
index 557785f..396368e 100644
--- a/apps/web/app/(main)/profile/page.tsx
+++ b/apps/web/app/(main)/profile/page.tsx
@@ -1,5 +1,4 @@
-/* eslint-disable sonarjs/different-types-comparison */
-import { becomeMaster, updateName } from '@/actions/profile';
+import { becomeMaster, updateProfile } from '@/actions/profile';
import { ProfileField } from '@/components/profile/profile-field';
import { authOptions } from '@/config/auth';
import { getCustomer } from '@repo/graphql/api';
@@ -14,6 +13,8 @@ export default async function ProfilePage() {
const user = data.customers.at(0);
const photoUrl = user?.photoUrl ?? 'https://github.com/shadcn.png';
+ if (!user) return 'Профиль не найден';
+
return (
@@ -25,7 +26,13 @@ export default async function ProfilePage() {
{user?.name}
-
+
void;
+ readonly onChange?: (value: CustomerInput) => Promise | void;
readonly value: string;
};
export function ProfileField({
disabled = false,
+ fieldName,
id,
label,
onChange,
value: initialValue,
}: ProfileFieldProps) {
const [value, setValue] = useState(initialValue);
- const [isPending, startTransition] = useTransition();
+
+ const [isPending, setIsPending] = useState(false);
+ const debouncedCallback = useDebouncedCallback((newValue: string) => {
+ if (!onChange || !fieldName) return;
+
+ setIsPending(true);
+ const result = onChange({ [fieldName]: newValue });
+
+ if (result instanceof Promise) {
+ result.finally(() => setIsPending(false));
+ } else {
+ setIsPending(false);
+ }
+ }, 300);
+
+ const inputRef = useFocus(isPending);
const handleChange = (event: ChangeEvent) => {
const newValue = event.target.value;
setValue(newValue);
- if (onChange) {
- startTransition(() => {
- onChange(newValue);
- });
- }
+ debouncedCallback(newValue);
};
return (
@@ -39,8 +55,20 @@ export function ProfileField({
disabled={disabled || isPending}
id={id}
onChange={handleChange}
+ ref={inputRef}
value={value}
/>
);
}
+
+function useFocus(isPending: boolean) {
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [isPending]);
+ return inputRef;
+}
diff --git a/apps/web/package.json b/apps/web/package.json
index ec60d87..18a219a 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -24,6 +24,7 @@
"next-themes": "^0.4.4",
"react": "catalog:",
"react-dom": "catalog:",
+ "use-debounce": "^10.0.4",
"zod": "catalog:"
},
"devDependencies": {
diff --git a/packages/graphql/api/customer.ts b/packages/graphql/api/customer.ts
index 4ed3316..a0bd9cb 100644
--- a/packages/graphql/api/customer.ts
+++ b/packages/graphql/api/customer.ts
@@ -19,3 +19,12 @@ export async function getCustomer(variables: GQL.GetCustomerQueryVariables) {
variables,
});
}
+
+export async function updateCustomerProfile(variables: GQL.UpdateCustomerProfileMutationVariables) {
+ const { mutate } = await getClientWithToken();
+
+ return mutate({
+ mutation: GQL.UpdateCustomerProfileDocument,
+ variables,
+ });
+}
diff --git a/packages/graphql/graphql.config.cjs b/packages/graphql/graphql.config.cjs
index a9c2fad..c1d69b9 100644
--- a/packages/graphql/graphql.config.cjs
+++ b/packages/graphql/graphql.config.cjs
@@ -4,9 +4,10 @@ module.exports = {
generates: {
'./types/operations.generated.ts': {
config: {
- avoidOptionals: true,
+ avoidOptionals: false,
onlyOperationTypes: true,
useTypeImports: true,
+ maybeValue: 'T | null | undefined'
},
plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
},
diff --git a/packages/graphql/operations/customer.graphql b/packages/graphql/operations/customer.graphql
index 2d144ba..d6e9f15 100644
--- a/packages/graphql/operations/customer.graphql
+++ b/packages/graphql/operations/customer.graphql
@@ -1,22 +1,27 @@
+fragment CustomerProfile on Customer {
+ active
+ documentId
+ name
+ phone
+ photoUrl
+ role
+ telegramId
+}
+
mutation CreateCustomer($name: String!, $telegramId: Long!, $phone: String!) {
createCustomer(data: { name: $name, telegramId: $telegramId, phone: $phone, role: client }) {
- documentId
- name
- telegramId
- phone
- role
- active
- createdAt
- updatedAt
- publishedAt
+ ...CustomerProfile
}
}
query GetCustomer($telegramId: Long!) {
customers(filters: { telegramId: { eq: $telegramId } }) {
- name
- phone
- role
- photoUrl
+ ...CustomerProfile
+ }
+}
+
+mutation UpdateCustomerProfile($documentId: ID!, $data: CustomerInput!) {
+ updateCustomer(documentId: $documentId, data: $data) {
+ ...CustomerProfile
}
}
diff --git a/packages/graphql/types/operations.generated.ts b/packages/graphql/types/operations.generated.ts
index d960e41..caec0c5 100644
--- a/packages/graphql/types/operations.generated.ts
+++ b/packages/graphql/types/operations.generated.ts
@@ -1,6 +1,6 @@
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
-export type Maybe = T | null;
-export type InputMaybe = Maybe;
+export type Maybe = T | null | undefined;
+export type InputMaybe = T | null | undefined;
export type Exact = { [K in keyof T]: T[K] };
export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
@@ -19,121 +19,121 @@ export type Scalars = {
};
export type BlockFiltersInput = {
- and: InputMaybe>>;
- client: InputMaybe;
- createdAt: InputMaybe;
- dateend: InputMaybe;
- datestart: InputMaybe;
- documentId: InputMaybe;
- master: InputMaybe;
- not: InputMaybe;
- or: InputMaybe>>;
- orders: InputMaybe;
- publishedAt: InputMaybe;
- sessionsCompleted: InputMaybe;
- sessionsTotal: InputMaybe;
- state: InputMaybe;
- updatedAt: InputMaybe;
+ and?: InputMaybe>>;
+ client?: InputMaybe;
+ createdAt?: InputMaybe;
+ dateend?: InputMaybe;
+ datestart?: InputMaybe;
+ documentId?: InputMaybe;
+ master?: InputMaybe;
+ not?: InputMaybe;
+ or?: InputMaybe>>;
+ orders?: InputMaybe;
+ publishedAt?: InputMaybe;
+ sessionsCompleted?: InputMaybe;
+ sessionsTotal?: InputMaybe;
+ state?: InputMaybe;
+ updatedAt?: InputMaybe;
};
export type BlockInput = {
- client: InputMaybe;
- dateend: InputMaybe;
- datestart: InputMaybe;
- master: InputMaybe;
- orders: InputMaybe>>;
- publishedAt: InputMaybe;
- sessionsCompleted: InputMaybe;
- sessionsTotal: InputMaybe;
- state: InputMaybe;
+ client?: InputMaybe;
+ dateend?: InputMaybe;
+ datestart?: InputMaybe;
+ master?: InputMaybe;
+ orders?: InputMaybe>>;
+ publishedAt?: InputMaybe;
+ sessionsCompleted?: InputMaybe;
+ sessionsTotal?: InputMaybe;
+ state?: InputMaybe;
};
export type BooleanFilterInput = {
- and: InputMaybe>>;
- between: InputMaybe>>;
- contains: InputMaybe;
- containsi: InputMaybe;
- endsWith: InputMaybe;
- eq: InputMaybe;
- eqi: InputMaybe;
- gt: InputMaybe;
- gte: InputMaybe;
- in: InputMaybe>>;
- lt: InputMaybe;
- lte: InputMaybe;
- ne: InputMaybe;
- nei: InputMaybe;
- not: InputMaybe;
- notContains: InputMaybe;
- notContainsi: InputMaybe;
- notIn: InputMaybe>>;
- notNull: InputMaybe;
- null: InputMaybe;
- or: InputMaybe>>;
- startsWith: InputMaybe;
+ and?: InputMaybe>>;
+ between?: InputMaybe>>;
+ contains?: InputMaybe;
+ containsi?: InputMaybe;
+ endsWith?: InputMaybe;
+ eq?: InputMaybe;
+ eqi?: InputMaybe;
+ gt?: InputMaybe;
+ gte?: InputMaybe;
+ in?: InputMaybe>>;
+ lt?: InputMaybe;
+ lte?: InputMaybe;
+ ne?: InputMaybe;
+ nei?: InputMaybe;
+ not?: InputMaybe;
+ notContains?: InputMaybe;
+ notContainsi?: InputMaybe;
+ notIn?: InputMaybe>>;
+ notNull?: InputMaybe;
+ null?: InputMaybe;
+ or?: InputMaybe>>;
+ startsWith?: InputMaybe;
};
export type CustomerFiltersInput = {
- active: InputMaybe;
- and: InputMaybe>>;
- blocks: InputMaybe;
- clients: InputMaybe;
- createdAt: InputMaybe;
- documentId: InputMaybe;
- masters: InputMaybe;
- name: InputMaybe;
- not: InputMaybe;
- or: InputMaybe>>;
- orders: InputMaybe;
- phone: InputMaybe;
- photoUrl: InputMaybe;
- publishedAt: InputMaybe;
- role: InputMaybe;
- setting: InputMaybe;
- slots: InputMaybe;
- telegramId: InputMaybe;
- updatedAt: InputMaybe;
+ active?: InputMaybe;
+ and?: InputMaybe>>;
+ blocks?: InputMaybe;
+ clients?: InputMaybe;
+ createdAt?: InputMaybe;
+ documentId?: InputMaybe;
+ masters?: InputMaybe;
+ name?: InputMaybe;
+ not?: InputMaybe;
+ or?: InputMaybe>>;
+ orders?: InputMaybe;
+ phone?: InputMaybe;
+ photoUrl?: InputMaybe;
+ publishedAt?: InputMaybe;
+ role?: InputMaybe;
+ setting?: InputMaybe;
+ slots?: InputMaybe;
+ telegramId?: InputMaybe;
+ updatedAt?: InputMaybe;
};
export type CustomerInput = {
- active: InputMaybe;
- blocks: InputMaybe>>;
- clients: InputMaybe>>;
- masters: InputMaybe>>;
- name: InputMaybe;
- orders: InputMaybe>>;
- phone: InputMaybe;
- photoUrl: InputMaybe;
- publishedAt: InputMaybe;
- role: InputMaybe;
- setting: InputMaybe;
- slots: InputMaybe>>;
- telegramId: InputMaybe;
+ active?: InputMaybe;
+ blocks?: InputMaybe>>;
+ clients?: InputMaybe>>;
+ masters?: InputMaybe>>;
+ name?: InputMaybe;
+ orders?: InputMaybe>>;
+ phone?: InputMaybe;
+ photoUrl?: InputMaybe;
+ publishedAt?: InputMaybe;
+ role?: InputMaybe;
+ setting?: InputMaybe;
+ slots?: InputMaybe>>;
+ telegramId?: InputMaybe;
};
export type DateTimeFilterInput = {
- and: InputMaybe>>;
- between: InputMaybe>>;
- contains: InputMaybe;
- containsi: InputMaybe;
- endsWith: InputMaybe;
- eq: InputMaybe;
- eqi: InputMaybe;
- gt: InputMaybe;
- gte: InputMaybe;
- in: InputMaybe>>;
- lt: InputMaybe;
- lte: InputMaybe;
- ne: InputMaybe;
- nei: InputMaybe;
- not: InputMaybe;
- notContains: InputMaybe;
- notContainsi: InputMaybe;
- notIn: InputMaybe>>;
- notNull: InputMaybe;
- null: InputMaybe;
- or: InputMaybe>>;
- startsWith: InputMaybe;
+ and?: InputMaybe>>;
+ between?: InputMaybe>>;
+ contains?: InputMaybe;
+ containsi?: InputMaybe;
+ endsWith?: InputMaybe;
+ eq?: InputMaybe;
+ eqi?: InputMaybe;
+ gt?: InputMaybe;
+ gte?: InputMaybe;
+ in?: InputMaybe>>;
+ lt?: InputMaybe;
+ lte?: InputMaybe;
+ ne?: InputMaybe;
+ nei?: InputMaybe;
+ not?: InputMaybe;
+ notContains?: InputMaybe;
+ notContainsi?: InputMaybe;
+ notIn?: InputMaybe>>;
+ notNull?: InputMaybe;
+ null?: InputMaybe;
+ or?: InputMaybe>>;
+ startsWith?: InputMaybe;
};
export enum Enum_Block_State {
@@ -162,179 +162,179 @@ export enum Enum_Slot_State {
}
export type FileInfoInput = {
- alternativeText: InputMaybe;
- caption: InputMaybe;
- name: InputMaybe;
+ alternativeText?: InputMaybe;
+ caption?: InputMaybe;
+ name?: InputMaybe;
};
export type FloatFilterInput = {
- and: InputMaybe>>;
- between: InputMaybe>>;
- contains: InputMaybe;
- containsi: InputMaybe;
- endsWith: InputMaybe;
- eq: InputMaybe;
- eqi: InputMaybe;
- gt: InputMaybe;
- gte: InputMaybe;
- in: InputMaybe>>;
- lt: InputMaybe;
- lte: InputMaybe;
- ne: InputMaybe;
- nei: InputMaybe;
- not: InputMaybe;
- notContains: InputMaybe;
- notContainsi: InputMaybe;
- notIn: InputMaybe>>;
- notNull: InputMaybe;
- null: InputMaybe;
- or: InputMaybe>>;
- startsWith: InputMaybe;
+ and?: InputMaybe>>;
+ between?: InputMaybe>>;
+ contains?: InputMaybe;
+ containsi?: InputMaybe;
+ endsWith?: InputMaybe;
+ eq?: InputMaybe;
+ eqi?: InputMaybe;
+ gt?: InputMaybe;
+ gte?: InputMaybe;
+ in?: InputMaybe>>;
+ lt?: InputMaybe;
+ lte?: InputMaybe;
+ ne?: InputMaybe