added profile description editing, fixed light theme and viewer caching
This commit is contained in:
parent
58a211663a
commit
c8f68b7486
10 changed files with 187 additions and 18 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
36
app/server/graphql/resolvers/userUpdateResolver.ts
Normal file
36
app/server/graphql/resolvers/userUpdateResolver.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue