added profile description editing, fixed light theme and viewer caching

This commit is contained in:
lnn0q 2025-06-17 21:46:40 +03:00
parent 58a211663a
commit c8f68b7486
10 changed files with 187 additions and 18 deletions

View file

@ -24,7 +24,7 @@ export function BookChapterCard({
refetchQueries: [bookChapterListQuery, bookChapterQuery]
});
return (
<Card.Root width="100%" variant="elevated">
<Card.Root width="100%" variant="elevated" bg={{ _light: 'gray.100' }}>
<Card.Body paddingX={4} paddingY={2}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Box>

View file

@ -1,6 +1,6 @@
import { Button, Card, Stack, Text, Wrap } from '@chakra-ui/react';
import isHotkey from 'is-hotkey';
import React, { KeyboardEvent, MouseEvent, useCallback, useMemo } from 'react';
import React, { KeyboardEvent, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import { createEditor, Descendant, Editor, Element as SlateElement, Transforms } from 'slate';
import { withHistory } from 'slate-history';
import {

View file

@ -19,7 +19,7 @@ export function WorkingListCard({
reviewsTotal
}: BookOne) {
return (
<Card.Root width="100%" variant="elevated">
<Card.Root width="100%" variant="elevated" bg={{ _light: 'gray.100' }}>
<Card.Body>
<Stack>
<Stack direction={'row'} justifyContent="space-between">

View file

@ -6,9 +6,21 @@ import { ColorModeProvider, type ColorModeProviderProps } from './color-mode';
export function Provider(props: ColorModeProviderProps) {
const system = createSystem(defaultConfig, {
theme: {
tokens: {}
tokens: {
colors: {
cyan: {
400: {
value: {
base: '#0c5c72',
_dark: '#67e8f9'
} as any // todo: find a better way to overwrite it
}
}
}
}
}
});
return (
<ChakraProvider value={system}>
<Theme colorPalette="cyan">

View file

@ -1,8 +1,26 @@
import { gql } from '@apollo/client';
import { Avatar } from '@app/client/components/ui/avatar';
import { userFragment } from '@app/client/fragments';
import { toFormattedDate } from '@app/client/helpers';
import { useIdentity } from '@app/client/hooks/identity';
import { useToastMutation } from '@app/client/hooks/useToastMutation';
import { useUserOneQuery } from '@app/client/hooks/useUserOneQuery';
import { Card, Heading, HStack, SkeletonCircle, SkeletonText, Stack, Text } from '@chakra-ui/react';
import {
Box,
Button,
Card,
Heading,
HStack,
Icon,
SkeletonCircle,
SkeletonText,
Stack,
Text,
Textarea
} from '@chakra-ui/react';
import { UserProfileUpdateMutation } from '@generated/types';
import { createFileRoute } from '@tanstack/react-router';
import { useState } from 'react';
export const Route = createFileRoute('/profile/$username')({
component: RouteComponent
@ -36,6 +54,16 @@ function UserOverview({
readonly createdAt: string;
};
}) {
const identity = useIdentity();
const { update: userUpdate } = useToastMutation<UserProfileUpdateMutation>({
mutation: userUpdateMutation,
successMsg: 'Profile updated successfully!'
});
const [isEditMode, setIsEditMode] = useState<boolean>(false);
const [description, setDescription] = useState<string>(user.description ?? '');
return (
<Stack width={{ base: '100%', md: '80%' }} maxWidth="840px" gap={3}>
<Stack direction="row" alignItems="start">
@ -45,17 +73,92 @@ function UserOverview({
{user.username}
</Heading>
<Stack direction="row">
<Text fontWeight="bold">joined:</Text>
<Text color="fg.muted">{toFormattedDate({ value: user.createdAt })}</Text>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack direction="row">
<Text fontWeight="bold">joined:</Text>
<Text color="fg.muted">{toFormattedDate({ value: user.createdAt })}</Text>
</Stack>
{identity != null && isEditMode && (
<Stack>
<Stack direction="row" alignItems="center" justifyContent="end" gap={1}>
<Button
size="sm"
height={6}
bg="green.500"
padding={0}
onClick={async () => {
await userUpdate({
variables: {
input: {
id: user.id,
description
}
}
});
identity.refetch();
setIsEditMode(false);
}}
>
<Icon>
<i className="nf nf-fa-check" />
</Icon>
</Button>
<Button
size="sm"
height={6}
bg="red.500"
padding={0}
onClick={async () => {
setDescription(user.description ?? '');
setIsEditMode(false);
}}
>
<Icon>
<i className="nf nf-fa-close" />
</Icon>
</Button>
</Stack>
</Stack>
)}
{!isEditMode && identity?.data.id === user.id && (
<Button
size="sm"
height={6}
padding={0}
onClick={() => {
setIsEditMode(true);
}}
>
<Icon>
<i className="nf nf-fa-edit" />
</Icon>
</Button>
)}
</Stack>
</Stack>
</Stack>
<Card.Root variant="subtle" width="100%">
<Card.Body paddingX="3" paddingY="1">
<Text>{user.description ?? 'No user description =('}</Text>
</Card.Body>
</Card.Root>
{isEditMode ? (
<Stack width="100%" borderRadius="md" position="relative">
<Textarea
variant="subtle"
placeholder="Tell us about yourself!"
resize="none"
maxLength={700}
maxLines={10}
fontSize="md"
autoresize
value={description}
onChange={event => setDescription(event.target.value)}
/>
</Stack>
) : (
<Card.Root variant="subtle" width="100%">
<Card.Body paddingX="3" paddingY="1">
<Text>{user.description ?? 'No user description =('}</Text>
</Card.Body>
</Card.Root>
)}
</Stack>
);
}
@ -74,3 +177,12 @@ function UserOverviewSkeleton() {
</Stack>
);
}
const userUpdateMutation = gql`
mutation UserProfileUpdate($input: UserUpdateInput!) {
userUpdate(input: $input) {
...User
}
}
${userFragment}
`;

View file

@ -28,7 +28,7 @@ function BookChapterView({
}) {
const { data } = useQuery<BookChapterViewQuery>(bookChapterQuery, {
variables: { id: chapterId },
fetchPolicy: 'cache-and-network'
fetchPolicy: 'no-cache'
});
const { data: navigationData } = useQuery<BookChapterViewNavigationQuery>(

View file

@ -14,14 +14,15 @@ import { UserOneResolver } from './resolvers/userOneResolver';
import { LoginResolver } from './resolvers/auth/login';
import { LogoutResolver } from './resolvers/auth/logout';
import { SignUpResolver } from './resolvers/auth/signUp';
import { BookUpdateResolver } from './resolvers/bookUpdateResolver';
import { BookDeleteResolver } from './resolvers/bookDeleteResolver';
import { BookChapterCreateResolver } from './resolvers/bookChapterCreateResolver';
import { BookChapterDeleteResolver } from './resolvers/bookChapterDelete';
import { BookChapterUpdateResolver } from './resolvers/bookChapterUpdateResolver';
import { BookCreateResolver } from './resolvers/bookCreateResolver';
import { BookDeleteResolver } from './resolvers/bookDeleteResolver';
import { BookUpdateResolver } from './resolvers/bookUpdateResolver';
import { RatingSetResolver } from './resolvers/ratingSetResolver';
import { ReviewCreateResolver } from './resolvers/reviewCreateResolver';
import { UserUpdateResolver } from './resolvers/userUpdateResolver';
export const Query: QueryResolvers = {
bookOne: BookOneResolver,
@ -44,7 +45,8 @@ export const Mutation: MutationResolvers = {
bookChapterDelete: BookChapterDeleteResolver,
bookChapterUpdate: BookChapterUpdateResolver,
ratingSet: RatingSetResolver,
reviewCreate: ReviewCreateResolver
reviewCreate: ReviewCreateResolver,
userUpdate: UserUpdateResolver
};
export const Subscription = {};

View file

@ -6,6 +6,7 @@ export async function toUserSchema(model: User): Promise<UserSchema> {
id: model.id,
username: model.username,
createdAt: model.createdAt.toISOString(),
updatedAt: model.updatedAt.toISOString()
updatedAt: model.updatedAt.toISOString(),
description: model.description
};
}

View file

@ -0,0 +1,36 @@
import { ensureIdentity } from '@app/server/helpers';
import { IdentityContext } from '@app/server/identity';
import { prisma, uuid } from '@app/server/prisma/client';
import { MutationUserUpdateArgs, User } from '@generated/types';
import { toUserSchema } from './mappers/userMapper';
import { call } from '@app/common';
export async function UserUpdateResolver(
_: any,
{ input }: MutationUserUpdateArgs,
{ identity }: IdentityContext
): Promise<User> {
ensureIdentity(identity);
if (input.id != identity.id) throw new Error('Operation is not permitted.'); //todo: add admin access
if (input.description != null && input.description.length > 700)
throw new Error('Description is too long.');
const description = call((): string | null | undefined => {
if (input.description != null && input.description.length !== 0) return input.description;
if (input.description != null && input.description.length === 0) return null;
return undefined;
});
const updated = await prisma.user.update({
where: {
id: input.id
},
data: {
description
}
});
return toUserSchema(updated);
}

View file

@ -25,6 +25,7 @@ type Mutation {
reviewUpdate(input: ReviewUpdateInput!): Review!
reviewDelete(id: ID!): Void
ratingSet(input: RatingSetInput!): Void
userUpdate(input: UserUpdateInput!): User!
}
input SignUpInput {
@ -63,6 +64,11 @@ input UserOneInput {
username: String
}
input UserUpdateInput {
id: ID!
description: String
}
input PaginationFilterInput {
terms: String
offset: Int