diff --git a/app/client/components/BookOverviewTabs.tsx b/app/client/components/BookOverviewTabs.tsx
index 738f5f4..f8551e1 100644
--- a/app/client/components/BookOverviewTabs.tsx
+++ b/app/client/components/BookOverviewTabs.tsx
@@ -3,10 +3,12 @@ import { Link } from '@tanstack/react-router';
export function BookOverviewTabs({
bookId,
- currentTab
+ currentTab,
+ isAuthor
}: {
readonly bookId: string;
- readonly currentTab: 'contents' | 'reviews';
+ readonly currentTab: 'contents' | 'reviews' | 'details';
+ readonly isAuthor: boolean;
}) {
return (
@@ -17,6 +19,11 @@ export function BookOverviewTabs({
Reviews
+ {isAuthor && (
+
+ Details
+
+ )}
);
diff --git a/app/client/main.tsx b/app/client/main.tsx
index ebce8f8..3c16175 100644
--- a/app/client/main.tsx
+++ b/app/client/main.tsx
@@ -1,7 +1,7 @@
import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache, from } from '@apollo/client';
import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev';
import { onError } from '@apollo/client/link/error';
-import { Box, Center, Spinner } from '@chakra-ui/react';
+import { Box, Center, Spinner, Stack } from '@chakra-ui/react';
import '@client/assets/styles/index.css';
import '@client/assets/styles/nerd-fonts-icons.css';
import { Provider as ChakraProvider } from '@client/components/ui/provider';
@@ -118,7 +118,9 @@ function App() {
-
+
+
+
diff --git a/app/client/pages/profile/$username.tsx b/app/client/pages/profile/$username.tsx
index f53d5af..ef20109 100644
--- a/app/client/pages/profile/$username.tsx
+++ b/app/client/pages/profile/$username.tsx
@@ -1,4 +1,5 @@
-import { createFileRoute } from '@tanstack/react-router';
+import {Stack} from '@chakra-ui/react';
+import {createFileRoute} from '@tanstack/react-router';
export const Route = createFileRoute('/profile/$username')({
component: RouteComponent
@@ -6,5 +7,5 @@ export const Route = createFileRoute('/profile/$username')({
function RouteComponent() {
const { username } = Route.useParams();
- return
{`Hello, "${username}"!`}
;
+ return {`Hello, "${username}"!`};
}
diff --git a/app/client/pages/work/book/$bookId/overview/contents.tsx b/app/client/pages/work/book/$bookId/overview/contents.tsx
index fdb37ac..408f1a5 100644
--- a/app/client/pages/work/book/$bookId/overview/contents.tsx
+++ b/app/client/pages/work/book/$bookId/overview/contents.tsx
@@ -32,10 +32,14 @@ function RouteComponent() {
return (
<>
-
-
{data != null ? (
<>
+
+
(bookQuery, {
+ variables: { id }
+ });
+
+ const { update: bookUpdate } = useToastMutation({
+ mutation: bookUpdateMutation,
+ successMsg: 'Book updated successfully'
+ });
+
+ const { update: bookDelete } = useToastMutation({
+ mutation: bookDeleteMutation,
+ successMsg: 'Book deleted successfully',
+ refetchQueries: [bookListQuery]
+ });
+
+ const [title, setTitle] = useState(data?.bookOne.title);
+ const [description, setDescription] = useState(data?.bookOne.description);
+
+ const [progress, setProgress] = useState(
+ data?.bookOne.progressStatus
+ );
+
+ if (data == null) return ;
+
+ return (
+ <>
+
+
+
+ setTitle(event.currentTarget.value)}
+ placeholder="title"
+ maxLength={60}
+ />
+
+
+ {
+ const updatedStatus = event.currentTarget.value;
+ ensureProgressStatus(updatedStatus);
+ setProgress(updatedStatus);
+ }}
+ >
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+function ensureProgressStatus(value: string): asserts value is WorkingProgressStatus {
+ if (value === WorkingProgressStatus.Complete) return;
+ if (value === WorkingProgressStatus.InProgress) return;
+ if (value === WorkingProgressStatus.Frozen) return;
+
+ throw new Error('Invalid WorkingPrgoressStatus');
+}
+
+const bookQuery = gql`
+ query BookDetails($id: ID!) {
+ bookOne(id: $id) {
+ ...Book
+ }
+ }
+ ${bookFragment}
+`;
+
+const bookUpdateMutation = gql`
+ mutation BookDetailsUpdate($input: BookUpdateInput!) {
+ bookUpdate(input: $input) {
+ ...Book
+ }
+ }
+ ${bookFragment}
+`;
+
+const bookDeleteMutation = gql`
+ mutation BookDetailsDelete($id: ID!) {
+ bookDelete(id: $id)
+ }
+`;
diff --git a/app/client/pages/work/book/$bookId/overview/reviews.tsx b/app/client/pages/work/book/$bookId/overview/reviews.tsx
index 4e34b62..faaccf1 100644
--- a/app/client/pages/work/book/$bookId/overview/reviews.tsx
+++ b/app/client/pages/work/book/$bookId/overview/reviews.tsx
@@ -1,4 +1,4 @@
-import { gql } from '@apollo/client';
+import { gql, useQuery } from '@apollo/client';
import { BookOverviewTabs } from '@app/client/components/BookOverviewTabs';
import { Pagination } from '@app/client/components/Pagination';
import { reviewFragment } from '@app/client/fragments';
@@ -6,7 +6,7 @@ import { useIdentity } from '@app/client/hooks/identity';
import { usePaginatedQuery } from '@app/client/hooks/usePaginatedQuery';
import { useToastMutation } from '@app/client/hooks/useToastMutation';
import { Skeleton, Stack, Text } from '@chakra-ui/react';
-import { BookReviewCreateMutation, BookReviewsQuery } from '@generated/types';
+import { BookReviewCreateMutation, BookReviewsDataQuery, BookReviewsQuery } from '@generated/types';
import { createFileRoute } from '@tanstack/react-router';
import { useState } from 'react';
import { bookQuery } from '../overview';
@@ -36,17 +36,25 @@ function RouteComponent() {
}
});
+ const { data: authorData } = useQuery(bookAuthorQuery, {
+ variables: { id }
+ });
+
const { update: reviewCreate } = useToastMutation({
mutation: bookReviewCreateMutation,
successMsg: 'Review posted!',
refetchQueries: [bookReviewsQuery, bookQuery]
});
- if (data == null) return ;
+ if (data == null || authorData == null) return ;
return (
<>
-
+
{identity != null && reviewsPage === 1 && (
WorkBookBookIdOverviewRoute,
} as any)
+const WorkBookBookIdOverviewDetailsRoute =
+ WorkBookBookIdOverviewDetailsImport.update({
+ id: '/details',
+ path: '/details',
+ getParentRoute: () => WorkBookBookIdOverviewRoute,
+ } as any)
+
const WorkBookBookIdOverviewContentsRoute =
WorkBookBookIdOverviewContentsImport.update({
id: '/contents',
@@ -183,6 +191,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof WorkBookBookIdOverviewContentsImport
parentRoute: typeof WorkBookBookIdOverviewImport
}
+ '/work/book/$bookId/overview/details': {
+ id: '/work/book/$bookId/overview/details'
+ path: '/details'
+ fullPath: '/work/book/$bookId/overview/details'
+ preLoaderRoute: typeof WorkBookBookIdOverviewDetailsImport
+ parentRoute: typeof WorkBookBookIdOverviewImport
+ }
'/work/book/$bookId/overview/reviews': {
id: '/work/book/$bookId/overview/reviews'
path: '/reviews'
@@ -211,12 +226,14 @@ declare module '@tanstack/react-router' {
interface WorkBookBookIdOverviewRouteChildren {
WorkBookBookIdOverviewContentsRoute: typeof WorkBookBookIdOverviewContentsRoute
+ WorkBookBookIdOverviewDetailsRoute: typeof WorkBookBookIdOverviewDetailsRoute
WorkBookBookIdOverviewReviewsRoute: typeof WorkBookBookIdOverviewReviewsRoute
}
const WorkBookBookIdOverviewRouteChildren: WorkBookBookIdOverviewRouteChildren =
{
WorkBookBookIdOverviewContentsRoute: WorkBookBookIdOverviewContentsRoute,
+ WorkBookBookIdOverviewDetailsRoute: WorkBookBookIdOverviewDetailsRoute,
WorkBookBookIdOverviewReviewsRoute: WorkBookBookIdOverviewReviewsRoute,
}
@@ -236,6 +253,7 @@ export interface FileRoutesByFullPath {
'/work/book/$bookId/overview': typeof WorkBookBookIdOverviewRouteWithChildren
'/work/book/$bookId/chapter/new': typeof WorkBookBookIdChapterNewRoute
'/work/book/$bookId/overview/contents': typeof WorkBookBookIdOverviewContentsRoute
+ '/work/book/$bookId/overview/details': typeof WorkBookBookIdOverviewDetailsRoute
'/work/book/$bookId/overview/reviews': typeof WorkBookBookIdOverviewReviewsRoute
'/work/book/$bookId/chapter/$chapterId/edit': typeof WorkBookBookIdChapterChapterIdEditRoute
'/work/book/$bookId/chapter/$chapterId/view': typeof WorkBookBookIdChapterChapterIdViewRoute
@@ -252,6 +270,7 @@ export interface FileRoutesByTo {
'/work/book/$bookId/overview': typeof WorkBookBookIdOverviewRouteWithChildren
'/work/book/$bookId/chapter/new': typeof WorkBookBookIdChapterNewRoute
'/work/book/$bookId/overview/contents': typeof WorkBookBookIdOverviewContentsRoute
+ '/work/book/$bookId/overview/details': typeof WorkBookBookIdOverviewDetailsRoute
'/work/book/$bookId/overview/reviews': typeof WorkBookBookIdOverviewReviewsRoute
'/work/book/$bookId/chapter/$chapterId/edit': typeof WorkBookBookIdChapterChapterIdEditRoute
'/work/book/$bookId/chapter/$chapterId/view': typeof WorkBookBookIdChapterChapterIdViewRoute
@@ -269,6 +288,7 @@ export interface FileRoutesById {
'/work/book/$bookId/overview': typeof WorkBookBookIdOverviewRouteWithChildren
'/work/book/$bookId/chapter/new': typeof WorkBookBookIdChapterNewRoute
'/work/book/$bookId/overview/contents': typeof WorkBookBookIdOverviewContentsRoute
+ '/work/book/$bookId/overview/details': typeof WorkBookBookIdOverviewDetailsRoute
'/work/book/$bookId/overview/reviews': typeof WorkBookBookIdOverviewReviewsRoute
'/work/book/$bookId/chapter/$chapterId/edit': typeof WorkBookBookIdChapterChapterIdEditRoute
'/work/book/$bookId/chapter/$chapterId/view': typeof WorkBookBookIdChapterChapterIdViewRoute
@@ -287,6 +307,7 @@ export interface FileRouteTypes {
| '/work/book/$bookId/overview'
| '/work/book/$bookId/chapter/new'
| '/work/book/$bookId/overview/contents'
+ | '/work/book/$bookId/overview/details'
| '/work/book/$bookId/overview/reviews'
| '/work/book/$bookId/chapter/$chapterId/edit'
| '/work/book/$bookId/chapter/$chapterId/view'
@@ -302,6 +323,7 @@ export interface FileRouteTypes {
| '/work/book/$bookId/overview'
| '/work/book/$bookId/chapter/new'
| '/work/book/$bookId/overview/contents'
+ | '/work/book/$bookId/overview/details'
| '/work/book/$bookId/overview/reviews'
| '/work/book/$bookId/chapter/$chapterId/edit'
| '/work/book/$bookId/chapter/$chapterId/view'
@@ -317,6 +339,7 @@ export interface FileRouteTypes {
| '/work/book/$bookId/overview'
| '/work/book/$bookId/chapter/new'
| '/work/book/$bookId/overview/contents'
+ | '/work/book/$bookId/overview/details'
| '/work/book/$bookId/overview/reviews'
| '/work/book/$bookId/chapter/$chapterId/edit'
| '/work/book/$bookId/chapter/$chapterId/view'
@@ -401,6 +424,7 @@ export const routeTree = rootRoute
"filePath": "work/book/$bookId/overview.tsx",
"children": [
"/work/book/$bookId/overview/contents",
+ "/work/book/$bookId/overview/details",
"/work/book/$bookId/overview/reviews"
]
},
@@ -411,6 +435,10 @@ export const routeTree = rootRoute
"filePath": "work/book/$bookId/overview/contents.tsx",
"parent": "/work/book/$bookId/overview"
},
+ "/work/book/$bookId/overview/details": {
+ "filePath": "work/book/$bookId/overview/details.tsx",
+ "parent": "/work/book/$bookId/overview"
+ },
"/work/book/$bookId/overview/reviews": {
"filePath": "work/book/$bookId/overview/reviews.tsx",
"parent": "/work/book/$bookId/overview"
diff --git a/app/server/graphql/graphql.ts b/app/server/graphql/graphql.ts
index 21296a4..da01ab8 100644
--- a/app/server/graphql/graphql.ts
+++ b/app/server/graphql/graphql.ts
@@ -14,6 +14,8 @@ 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';
@@ -36,6 +38,8 @@ export const Mutation: MutationResolvers = {
login: LoginResolver,
logout: LogoutResolver,
bookCreate: BookCreateResolver,
+ bookUpdate: BookUpdateResolver,
+ bookDelete: BookDeleteResolver,
bookChapterCreate: BookChapterCreateResolver,
bookChapterDelete: BookChapterDeleteResolver,
bookChapterUpdate: BookChapterUpdateResolver,
diff --git a/app/server/graphql/resolvers/bookDeleteResolver.ts b/app/server/graphql/resolvers/bookDeleteResolver.ts
new file mode 100644
index 0000000..46a796f
--- /dev/null
+++ b/app/server/graphql/resolvers/bookDeleteResolver.ts
@@ -0,0 +1,22 @@
+import { ensureIdentity } from '@app/server/helpers';
+import { IdentityContext } from '@app/server/identity';
+import { prisma } from '@app/server/prisma/client';
+import { MutationBookDeleteArgs } from '@generated/types';
+
+export async function BookDeleteResolver(
+ _: any,
+ { id }: MutationBookDeleteArgs,
+ { identity }: IdentityContext
+): Promise {
+ ensureIdentity(identity);
+
+ const model = await prisma.book.findUniqueOrThrow({
+ where: { id }
+ });
+
+ if (identity.id !== model.authorId) throw new Error('Operation is not permitted.');
+
+ await prisma.book.delete({
+ where: { id }
+ });
+}
diff --git a/app/server/graphql/resolvers/bookUpdateResolver.ts b/app/server/graphql/resolvers/bookUpdateResolver.ts
new file mode 100644
index 0000000..e969c18
--- /dev/null
+++ b/app/server/graphql/resolvers/bookUpdateResolver.ts
@@ -0,0 +1,34 @@
+import { ensureIdentity } from '@app/server/helpers';
+import { IdentityContext } from '@app/server/identity';
+import { prisma } from '@app/server/prisma/client';
+import { Book, MutationBookUpdateArgs } from '@generated/types';
+import { toBookSchema } from './mappers/bookMapper';
+
+export async function BookUpdateResolver(
+ _: any,
+ { input }: MutationBookUpdateArgs,
+ { req, res, identity }: IdentityContext
+): Promise {
+ ensureIdentity(identity);
+
+ const model = await prisma.book.findUniqueOrThrow({
+ where: { id: input.id }
+ });
+
+ if (identity.id !== model.authorId) throw new Error('Operation is not permitted.');
+
+ if (input.title != null && input.title.length > 60) throw new Error('Title is too long.');
+ if (input.description != null && input.description.length > 900)
+ throw new Error('Description is too long.');
+
+ const modelUpdated = await prisma.book.update({
+ where: { id: input.id },
+ data: {
+ title: input.title != null ? input.title : undefined,
+ description: input.description != null ? input.description : undefined,
+ progressStatus: input.progressStatus != null ? input.progressStatus : undefined
+ }
+ });
+
+ return toBookSchema(modelUpdated, { req, res, identity });
+}
diff --git a/app/server/graphql/schema.graphql b/app/server/graphql/schema.graphql
index f21a5c2..8aa55d3 100644
--- a/app/server/graphql/schema.graphql
+++ b/app/server/graphql/schema.graphql
@@ -16,6 +16,8 @@ type Mutation {
login(input: LoginInput!): Session!
logout: Void
bookCreate(input: BookCreateInput!): Book!
+ bookUpdate(input: BookUpdateInput!): Book!
+ bookDelete(id: ID!): Void
bookChapterCreate(input: BookChapterCreateInput!): BookChapter!
bookChapterUpdate(input: BookChapterUpdateInput!): BookChapter!
bookChapterDelete(id: ID!): Book!
@@ -97,7 +99,7 @@ type Book {
personalRating: RatingType!
}
-input BookUpdate {
+input BookUpdateInput {
id: ID!
title: String
description: String
diff --git a/app/server/prisma/schema.prisma b/app/server/prisma/schema.prisma
index 695cbcd..5f1d237 100644
--- a/app/server/prisma/schema.prisma
+++ b/app/server/prisma/schema.prisma
@@ -74,7 +74,7 @@ model BookChapter {
wordsTotal Int @default(0)
- book Book @relation(fields: [bookId], references: [id])
+ book Book @relation(fields: [bookId], references: [id], onDelete: Cascade)
bookId String @db.VarChar(255)
reviews Review[]
@@ -94,10 +94,10 @@ model Review {
user User @relation(fields: [userId], references: [id])
userId String @db.VarChar(255)
- bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id])
+ bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id], onDelete: Cascade)
bookChapterId String? @db.VarChar(255)
- book Book? @relation(fields: [bookId], references: [id])
+ book Book? @relation(fields: [bookId], references: [id], onDelete: Cascade)
bookId String?
}