diff --git a/README.md b/README.md index 0e8c08fd..328dd8c4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

모두의 글을 엮어 만드는 나만의 책, 노티클 📒

Documentation
-Explore Knoticle +Explore Knoticle diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index 4af6a157..76891f57 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -25,6 +25,12 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', + 'padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, + { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, + { blankLine: 'always', prev: '*', next: 'return' }, + ], 'prettier/prettier': ['error', { endOfLine: 'auto' }], 'import/order': [ 'error', diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 4ad8e7e0..70d9fdf0 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -52,8 +52,6 @@ model Article { book_id Int scraps Scrap[] - @@fulltext([content]) - @@fulltext([title]) @@fulltext([content, title]) } diff --git a/backend/src/apis/articles/articles.controller.ts b/backend/src/apis/articles/articles.controller.ts index b6033450..b38b5ca6 100644 --- a/backend/src/apis/articles/articles.controller.ts +++ b/backend/src/apis/articles/articles.controller.ts @@ -10,14 +10,14 @@ const searchArticles = async (req: Request, res: Response) => { const searchResult = await articlesService.searchArticles({ query, page, take: +take, userId }); - res.status(200).send(searchResult); + return res.status(200).send(searchResult); }; const getArticle = async (req: Request, res: Response) => { const articleId = Number(req.params.articleId); const articleData = await articlesService.getArticle(articleId); - res.status(200).send(articleData); + return res.status(200).send(articleData); }; const createArticle = async (req: Request, res: Response) => { @@ -28,23 +28,46 @@ const createArticle = async (req: Request, res: Response) => { content: article.content, book_id: article.book_id, }); + // forEach와 async,await을 같이사용하는 것이 맞나? 다른방법은 없나? + scraps.forEach(async (scrap: IScrap) => { + if (scrap.id === 0) { + await scrapsService.createScrap({ + order: scrap.order, + is_original: true, + book_id: article.book_id, + article_id: createdArticle.id, + }); + } else { + await scrapsService.updateScrapOrder(scrap); + } + }); + + return res.status(201).send({ createdArticle }); +}; + +const updateArticle = async (req: Request, res: Response) => { + const { article, scraps } = req.body; + + const articleId = Number(req.params.articleId); + + const modifiedArticle = await articlesService.updateArticle(articleId, { + title: article.title, + content: article.content, + book_id: article.book_id, + }); + const result: any[] = []; + scraps.forEach(async (scrap: IScrap) => { if (scrap.id === 0) { - result.push( - await scrapsService.createScrap({ - order: scrap.order, - is_original: true, - book_id: article.book_id, - article_id: createdArticle.id, - }) - ); + result.push(await scrapsService.updateScrapBookId(articleId, article.book_id, scrap)); } else { - result.push(await scrapsService.updateScraps(scrap)); + result.push(await scrapsService.updateScrapOrder(scrap)); } }); - res.status(201).send({ createdArticle, result }); + + return res.status(201).send({ modifiedArticle, result }); }; const deleteArticle = async (req: Request, res: Response) => { @@ -52,34 +75,39 @@ const deleteArticle = async (req: Request, res: Response) => { await articlesService.deleteArticle(articleId); - res.status(204).send(); + return res.status(204).send(); }; const getTemporaryArticle = async (req: Request, res: Response) => { - const userId = Number(req.params.userId); + if (!res.locals.user) return res.status(200).send(); + const userId = res.locals.user.id; const temporaryArticle = await articlesService.getTemporaryArticle(userId); - res.status(200).send({ temporaryArticle }); + return res.status(200).send(temporaryArticle); }; -const craeteTemporaryArticle = async (req: Request, res: Response) => { - const { title, content, user_id } = req.body; +const createTemporaryArticle = async (req: Request, res: Response) => { + const { title, content } = req.body; + + const userId = res.locals.user.id; const temporaryArticle = await articlesService.createTemporaryArticle({ title, content, - user_id, + user_id: userId, }); - res.status(201).send({ temporaryArticle }); + return res.status(201).send(temporaryArticle); + res.status(201).send(temporaryArticle); }; export default { searchArticles, getArticle, createArticle, + updateArticle, deleteArticle, getTemporaryArticle, - craeteTemporaryArticle, + createTemporaryArticle, }; diff --git a/backend/src/apis/articles/articles.service.ts b/backend/src/apis/articles/articles.service.ts index ffca3951..89899603 100644 --- a/backend/src/apis/articles/articles.service.ts +++ b/backend/src/apis/articles/articles.service.ts @@ -49,6 +49,13 @@ const searchArticles = async (searchArticles: SearchArticles) => { deleted_at: null, ...matchUserCondition, }, + orderBy: { + _relevance: { + fields: ['title', 'content'], + sort: 'desc', + search: `${query}*`, + }, + }, take, skip, }); @@ -100,6 +107,7 @@ const createArticle = async (dto: CreateArticle) => { }, }, }); + return article; }; @@ -149,10 +157,32 @@ const createTemporaryArticle = async (dto: CreateTemporaryArticle) => { return temporaryArticle; }; +const updateArticle = async (articleId: number, dto: CreateArticle) => { + const { title, content, book_id } = dto; + + const article = await prisma.article.update({ + where: { + id: articleId, + }, + data: { + title, + content, + book: { + connect: { + id: book_id, + }, + }, + }, + }); + + return article; +}; + export default { searchArticles, getArticle, createArticle, + updateArticle, deleteArticle, getTemporaryArticle, createTemporaryArticle, diff --git a/backend/src/apis/auth/auth.controller.ts b/backend/src/apis/auth/auth.controller.ts index cc37c24e..3b8de7fb 100644 --- a/backend/src/apis/auth/auth.controller.ts +++ b/backend/src/apis/auth/auth.controller.ts @@ -20,7 +20,7 @@ const signIn = async (req: Request, res: Response) => { res.cookie('access_token', accessToken, { httpOnly: true }); res.cookie('refresh_token', refreshToken, { httpOnly: true }); - res.status(200).send({ id: user.id, nickname: user.nickname }); + return res.status(200).send({ id: user.id, nickname: user.nickname }); }; const signInGithub = async (req: Request, res: Response) => { @@ -41,7 +41,7 @@ const signInGithub = async (req: Request, res: Response) => { res.cookie('access_token', accessToken, { httpOnly: true }); res.cookie('refresh_token', refreshToken, { httpOnly: true }); - res.status(200).send({ id: githubUser.id, nickname: githubUser.nickname }); + return res.status(200).send({ id: githubUser.id, nickname: githubUser.nickname }); }; const signUp = async (req: Request, res: Response) => { @@ -51,7 +51,7 @@ const signUp = async (req: Request, res: Response) => { await authService.signUpLocalUser(username, password, nickname); - res.status(201).send(); + return res.status(201).send(); }; const checkSignInStatus = async (req: Request, res: Response) => { @@ -62,14 +62,15 @@ const checkSignInStatus = async (req: Request, res: Response) => { return res.status(200).send({ id: user.id, nickname: user.nickname }); } - res.status(200).send({ id: 0, nickname: '' }); + + return res.status(200).send({ id: 0, nickname: '' }); }; const signOut = async (req: Request, res: Response) => { res.clearCookie('access_token'); res.clearCookie('refresh_token'); - res.status(200).send({ id: 0, nickname: '' }); + return res.status(200).send({ id: 0, nickname: '' }); }; export default { diff --git a/backend/src/apis/auth/auth.service.ts b/backend/src/apis/auth/auth.service.ts index 7b39194b..62827764 100644 --- a/backend/src/apis/auth/auth.service.ts +++ b/backend/src/apis/auth/auth.service.ts @@ -38,6 +38,7 @@ const getGithubAccessToken = async (code: string) => { }, } ); + return data.access_token; }; @@ -60,6 +61,7 @@ const getUserByLocalDB = async (provider_id: string) => { nickname: true, }, }); + return user; }; @@ -72,6 +74,11 @@ const signUpGithubUser = async (username: string, provider_id: string) => { provider: 'github', password: '', description: `안녕하세요 ${nickname}입니다.`, + books: { + create: { + title: '새로운 책', + }, + }, }, }); @@ -129,6 +136,11 @@ const signUpLocalUser = async (username: string, password: string, nickname: str provider: 'local', password: encryptedPassword, description: `안녕하세요 ${nickname}입니다.`, + books: { + create: { + title: '새로운 책', + }, + }, }, }); }; diff --git a/backend/src/apis/bookmarks/bookmarks.controller.ts b/backend/src/apis/bookmarks/bookmarks.controller.ts index 5c0fa70f..8191114e 100644 --- a/backend/src/apis/bookmarks/bookmarks.controller.ts +++ b/backend/src/apis/bookmarks/bookmarks.controller.ts @@ -7,13 +7,16 @@ const createBookmark = async (req: Request, res: Response) => { const userId = res.locals.user.id; const bookmarkId = await bookmarksService.createBookmark(Number(userId), Number(book_id)); - res.status(200).send({ bookmarkId }); + + return res.status(200).send({ bookmarkId }); }; const deleteBookmark = async (req: Request, res: Response) => { const bookmarkId = Number(req.params.bookmarkId); + await bookmarksService.deleteBookmark(bookmarkId); - res.status(200).send(); + + return res.status(200).send(); }; export default { diff --git a/backend/src/apis/bookmarks/bookmarks.service.ts b/backend/src/apis/bookmarks/bookmarks.service.ts index d39b8474..bb4b8ce5 100644 --- a/backend/src/apis/bookmarks/bookmarks.service.ts +++ b/backend/src/apis/bookmarks/bookmarks.service.ts @@ -8,6 +8,7 @@ const createBookmark = async (user_id: number, book_id: number) => { book_id, }, }); + return id; }; diff --git a/backend/src/apis/books/books.controller.ts b/backend/src/apis/books/books.controller.ts index da536dec..a7ddd4d8 100644 --- a/backend/src/apis/books/books.controller.ts +++ b/backend/src/apis/books/books.controller.ts @@ -12,9 +12,9 @@ const getBook = async (req: Request, res: Response) => { if (!userId) userId = 0; - const book = await booksService.findBook(+bookId, userId); + const book = await booksService.getBook(+bookId, userId); - res.status(200).send(book); + return res.status(200).send(book); }; const getBooks = async (req: Request, res: Response) => { @@ -24,9 +24,9 @@ const getBooks = async (req: Request, res: Response) => { if (!userId) userId = 0; - const books = await booksService.findBooks({ order, take: +take, userId, editor, type }); + const books = await booksService.getBooks({ order, take: +take, userId, editor, type }); - res.status(200).send(books); + return res.status(200).send(books); }; const searchBooks = async (req: Request, res: Response) => { @@ -34,7 +34,7 @@ const searchBooks = async (req: Request, res: Response) => { const searchResult = await booksService.searchBooks({ query, userId, take: +take, page }); - res.status(200).send(searchResult); + return res.status(200).send(searchResult); }; const createBook = async (req: Request, res: Response) => { @@ -44,26 +44,25 @@ const createBook = async (req: Request, res: Response) => { const book = await booksService.createBook({ title, userId }); - const bookData = await booksService.findBook(book.id, userId); + const bookData = await booksService.getBook(book.id, userId); - res.status(201).send(bookData); + return res.status(201).send(bookData); }; -const editBook = async (req: Request, res: Response) => { +const updateBook = async (req: Request, res: Response) => { const { id, title, thumbnail_image, scraps } = req.body; const userId = res.locals.user.id; - const book = await booksService.editBook({ id, title, thumbnail_image }); + const book = await booksService.updateBook({ id, title, thumbnail_image }); - const result: any[] = []; scraps.forEach(async (scrap: IScrap) => { - result.push(await scrapsService.updateScraps(scrap)); + await scrapsService.updateScrapOrder(scrap); }); - const bookData = await booksService.findBook(book.id, userId); + const bookData = await booksService.getBook(book.id, userId); - res.status(200).send(bookData); + return res.status(200).send(bookData); }; const deleteBook = async (req: Request, res: Response) => { @@ -73,7 +72,7 @@ const deleteBook = async (req: Request, res: Response) => { const book = await booksService.deleteBook(bookId, userId); - res.status(200).send(book); + return res.status(200).send(book); }; export default { @@ -81,6 +80,6 @@ export default { getBooks, searchBooks, createBook, - editBook, + updateBook, deleteBook, }; diff --git a/backend/src/apis/books/books.service.ts b/backend/src/apis/books/books.service.ts index 1ca235a0..1a2f0145 100644 --- a/backend/src/apis/books/books.service.ts +++ b/backend/src/apis/books/books.service.ts @@ -2,34 +2,35 @@ import { FindBooks, SearchBooks, CreateBook } from '@apis/books/books.interface' import { prisma } from '@config/orm.config'; import { Message, NotFound } from '@errors'; -const findBook = async (bookId: number, userId: number) => { - const book = await prisma.book.findFirst({ +const searchBooks = async ({ query, userId, take, page }: SearchBooks) => { + const skip = (page - 1) * take; + + const books = await prisma.book.findMany({ select: { id: true, title: true, thumbnail_image: true, + created_at: true, user: { select: { nickname: true, - profile_image: true, }, }, scraps: { - orderBy: { order: 'asc' }, select: { + id: true, order: true, article: { select: { id: true, title: true, - deleted_at: true, }, }, }, }, bookmarks: { where: { - user_id: userId, + user_id: Number(userId) ? Number(userId) : 0, }, }, _count: { @@ -37,39 +38,50 @@ const findBook = async (bookId: number, userId: number) => { }, }, where: { - id: bookId, deleted_at: null, + user_id: Number(userId) ? Number(userId) : undefined, + title: { + search: `${query}*`, + }, + }, + orderBy: { + _relevance: { + fields: ['title'], + sort: 'desc', + search: `${query}*`, + }, }, + skip, + take, }); - return book; + return { + data: books, + hasNextPage: books.length === take, + }; }; -const findBooks = async ({ order, take, userId, editor, type }: FindBooks) => { - const sortOptions = []; - if (order === 'bookmark') sortOptions.push({ bookmarks: { _count: 'desc' as const } }); - if (order === 'newest') sortOptions.push({ created_at: 'desc' as const }); - - const books = await prisma.book.findMany({ +const getBook = async (bookId: number, userId: number) => { + const book = await prisma.book.findFirst({ select: { id: true, title: true, thumbnail_image: true, - created_at: true, user: { select: { nickname: true, + profile_image: true, }, }, scraps: { orderBy: { order: 'asc' }, select: { - id: true, order: true, article: { select: { id: true, title: true, + deleted_at: true, }, }, }, @@ -84,37 +96,19 @@ const findBooks = async ({ order, take, userId, editor, type }: FindBooks) => { }, }, where: { + id: bookId, deleted_at: null, - user: - type === 'bookmark' - ? {} - : { - is: { - nickname: editor ? editor : undefined, - }, - }, - bookmarks: - type === 'bookmark' - ? { - some: { - user: { - is: { - nickname: editor ? editor : undefined, - }, - }, - }, - } - : {}, }, - orderBy: sortOptions, - take, }); - return books; + return book; }; -const searchBooks = async ({ query, userId, take, page }: SearchBooks) => { - const skip = (page - 1) * take; +const getBooks = async ({ order, take, userId, editor, type }: FindBooks) => { + const sortOptions = []; + + if (order === 'bookmark') sortOptions.push({ bookmarks: { _count: 'desc' as const } }); + if (order === 'newest') sortOptions.push({ created_at: 'desc' as const }); const books = await prisma.book.findMany({ select: { @@ -128,9 +122,11 @@ const searchBooks = async ({ query, userId, take, page }: SearchBooks) => { }, }, scraps: { + orderBy: { order: 'asc' }, select: { id: true, order: true, + is_original: true, article: { select: { id: true, @@ -141,7 +137,7 @@ const searchBooks = async ({ query, userId, take, page }: SearchBooks) => { }, bookmarks: { where: { - user_id: Number(userId) ? Number(userId) : 0, + user_id: userId, }, }, _count: { @@ -150,19 +146,32 @@ const searchBooks = async ({ query, userId, take, page }: SearchBooks) => { }, where: { deleted_at: null, - user_id: Number(userId) ? Number(userId) : undefined, - title: { - search: `${query}*`, - }, + user: + type === 'bookmark' + ? {} + : { + is: { + nickname: editor ? editor : undefined, + }, + }, + bookmarks: + type === 'bookmark' + ? { + some: { + user: { + is: { + nickname: editor ? editor : undefined, + }, + }, + }, + } + : {}, }, - skip, + orderBy: sortOptions, take, }); - return { - data: books, - hasNextPage: books.length === take, - }; + return books; }; const createBook = async ({ title, userId }: CreateBook) => { @@ -182,7 +191,7 @@ const createBook = async ({ title, userId }: CreateBook) => { return book; }; -const editBook = async (dto: any) => { +const updateBook = async (dto: any) => { const { id, title, thumbnail_image } = dto; const book = await prisma.book.update({ where: { @@ -224,10 +233,10 @@ const checkBookOwnerCorrect = async (id: number, userId: number) => { }; export default { - findBook, - findBooks, searchBooks, + getBook, + getBooks, createBook, - editBook, + updateBook, deleteBook, }; diff --git a/backend/src/apis/images/images.controller.ts b/backend/src/apis/images/images.controller.ts index 444e178c..a3b1e2f3 100644 --- a/backend/src/apis/images/images.controller.ts +++ b/backend/src/apis/images/images.controller.ts @@ -5,7 +5,7 @@ import imagesService from '@apis/images/images.service'; const createImage = async (req: Request, res: Response) => { const imagePath = await imagesService.createImage({ file: req.file }); - res.status(201).send({ imagePath }); + return res.status(201).send({ imagePath }); }; export default { diff --git a/backend/src/apis/index.ts b/backend/src/apis/index.ts index 47cfaa2e..312219cf 100644 --- a/backend/src/apis/index.ts +++ b/backend/src/apis/index.ts @@ -21,26 +21,29 @@ router.post('/auth/signup', catchAsync(authController.signUp)); router.get('/auth/signout', catchAsync(authController.signOut)); router.get('/auth', decoder, catchAsync(authController.checkSignInStatus)); +router.get('/articles/temporary', guard, catchAsync(articlesController.getTemporaryArticle)); +router.post('/articles/temporary', guard, catchAsync(articlesController.createTemporaryArticle)); router.get('/articles/search', catchAsync(articlesController.searchArticles)); router.get('/articles/:articleId', catchAsync(articlesController.getArticle)); router.post('/articles', catchAsync(articlesController.createArticle)); +router.patch('/articles/:articleId', catchAsync(articlesController.updateArticle)); router.delete('/articles/:articleId', catchAsync(articlesController.deleteArticle)); -router.get('/articles/temporary/:userId', catchAsync(articlesController.getTemporaryArticle)); -router.post('/articles/temporary', catchAsync(articlesController.craeteTemporaryArticle)); router.post('/image', multer().single('image'), catchAsync(imagesController.createImage)); router.get('/books/search', catchAsync(booksController.searchBooks)); router.get('/books/:bookId', decoder, catchAsync(booksController.getBook)); -router.delete('/books/:bookId', catchAsync(guard), catchAsync(booksController.deleteBook)); router.get('/books', decoder, catchAsync(booksController.getBooks)); -router.post('/books', catchAsync(guard), catchAsync(booksController.createBook)); -router.patch('/books', catchAsync(guard), catchAsync(booksController.editBook)); +router.post('/books', guard, catchAsync(booksController.createBook)); +router.patch('/books', guard, catchAsync(booksController.updateBook)); +router.delete('/books/:bookId', guard, catchAsync(booksController.deleteBook)); -router.post('/bookmarks', catchAsync(guard), catchAsync(bookmarksController.createBookmark)); +router.post('/bookmarks', guard, catchAsync(bookmarksController.createBookmark)); router.delete('/bookmarks/:bookmarkId', catchAsync(bookmarksController.deleteBookmark)); +router.get('/scraps', catchAsync(scrapsController.getScraps)); router.post('/scraps', catchAsync(scrapsController.createScrap)); +router.delete('/scraps/:scrapId', guard, catchAsync(scrapsController.deleteScrap)); router.get('/users', catchAsync(usersController.getUserProfile)); router.patch('/users/:userId', catchAsync(usersController.editUserProfile)); diff --git a/backend/src/apis/scraps/scraps.controller.ts b/backend/src/apis/scraps/scraps.controller.ts index f1dfe3e5..80e765e4 100644 --- a/backend/src/apis/scraps/scraps.controller.ts +++ b/backend/src/apis/scraps/scraps.controller.ts @@ -7,25 +7,37 @@ import scrapsService from './scraps.service'; const createScrap = async (req: Request, res: Response) => { const { book_id, article_id, scraps } = req.body; - const result: any[] = []; scraps.forEach(async (scrap: IScrap) => { if (scrap.id === 0) { - result.push( - await scrapsService.createScrap({ - order: scrap.order, - is_original: true, - book_id, - article_id: article_id, - }) - ); + await scrapsService.createScrap({ + order: scrap.order, + is_original: false, + book_id, + article_id: article_id, + }); } else { - result.push(await scrapsService.updateScraps(scrap)); + await scrapsService.updateScrapOrder(scrap); } }); - res.status(201).send(); + return res.status(201).send(); +}; + +const deleteScrap = async (req: Request, res: Response) => { + const scrapId = Number(req.params.scrapId); + + await scrapsService.deleteScrap(scrapId); + + return res.status(200).send(); +}; +const getScraps = async (req: Request, res: Response) => { + const scraps = await scrapsService.getScraps(); + + return res.status(200).send(scraps); }; export default { createScrap, + deleteScrap, + getScraps, }; diff --git a/backend/src/apis/scraps/scraps.service.ts b/backend/src/apis/scraps/scraps.service.ts index 7751e7fc..14cbfb6b 100644 --- a/backend/src/apis/scraps/scraps.service.ts +++ b/backend/src/apis/scraps/scraps.service.ts @@ -39,7 +39,7 @@ const checkScrapExists = async (dto: CreateScrap) => { if (scrap) throw new ResourceConflict(Message.SCRAP_OVERLAP); }; -const updateScraps = async (scraps: IScrap) => { +const updateScrapOrder = async (scraps: IScrap) => { const scrap = await prisma.scrap.update({ where: { id: scraps.id, @@ -48,11 +48,64 @@ const updateScraps = async (scraps: IScrap) => { order: scraps.order, }, }); + + return scrap; +}; + +const deleteScrap = async (scrapId: number) => { + await prisma.scrap.delete({ + where: { + id: scrapId, + }, + }); +}; + +const getScraps = async () => { + const scraps = await prisma.scrap.findMany({ + select: { + book_id: true, + article_id: true, + }, + where: { + is_original: true, + book: { + deleted_at: null, + }, + article: { + deleted_at: null, + }, + }, + }); + + return scraps; +}; + +const updateScrapBookId = async (articleId: number, bookId: number, scraps: IScrap) => { + const originalScrap = await prisma.scrap.findFirst({ + where: { + is_original: true, + article_id: articleId, + }, + }); + + const scrap = await prisma.scrap.update({ + where: { + id: originalScrap.id, + }, + data: { + book_id: bookId, + order: scraps.order, + }, + }); + return scrap; }; export default { createScrap, checkScrapExists, - updateScraps, + updateScrapOrder, + updateScrapBookId, + deleteScrap, + getScraps, }; diff --git a/backend/src/apis/users/users.controller.ts b/backend/src/apis/users/users.controller.ts index c44d8eb7..0283f077 100644 --- a/backend/src/apis/users/users.controller.ts +++ b/backend/src/apis/users/users.controller.ts @@ -7,7 +7,7 @@ const getUserProfile = async (req: Request, res: Response) => { const userProfile = await usersService.findUserProfile(userNickname); - res.status(200).send(userProfile); + return res.status(200).send(userProfile); }; // PATCH인데 id가 req.body에 담겨서 오다보니 params를 확인할 일이 없는데... @@ -16,7 +16,7 @@ const editUserProfile = async (req: Request, res: Response) => { const userProfile = await usersService.updateUserProfile(req.body); - res.status(200).send(userProfile); + return res.status(200).send(userProfile); }; export default { diff --git a/backend/src/apis/users/users.service.ts b/backend/src/apis/users/users.service.ts index 8661f6c7..40555b47 100644 --- a/backend/src/apis/users/users.service.ts +++ b/backend/src/apis/users/users.service.ts @@ -25,6 +25,7 @@ const updateUserProfile = async (dto: UpdateUserProfile) => { const { id, nickname, profile_image, description } = dto; const user = await getUserByNickname(nickname); + if (user && user.id !== id) throw new ResourceConflict(Message.AUTH_NICKNAME_OVERLAP); const userProfile = await prisma.user.update({ diff --git a/backend/src/errors/message.ts b/backend/src/errors/message.ts index be7b4838..3e0006e2 100644 --- a/backend/src/errors/message.ts +++ b/backend/src/errors/message.ts @@ -6,7 +6,7 @@ export default { ARTICLE_NOTFOUND: '일치하는 글이 없습니다.', BOOK_NOTFOUND: '일치하는 책이 없습니다.', USER_NOTFOUND: '일치하는 유저가 없습니다.', - TOKEN_EXPIRED: '다시 로그인 해주세요', - TOKEN_MALFORMED: '다시 로그인 해주세요', + TOKEN_EXPIRED: '로그인이 필요합니다.', + TOKEN_MALFORMED: '로그인이 필요합니다.', BOOKMARK_NOTFOUND: '북마크된 책이 아닙니다.', }; diff --git a/backend/src/loaders/express.loader.ts b/backend/src/loaders/express.loader.ts index 0f99cbda..1ca12a28 100644 --- a/backend/src/loaders/express.loader.ts +++ b/backend/src/loaders/express.loader.ts @@ -6,6 +6,7 @@ import logger from 'morgan'; import router from '@apis'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars const errorHandler: ErrorRequestHandler = (err, req, res, next) => { const { status, message } = err; diff --git a/backend/src/middlewares/tokenValidator.ts b/backend/src/middlewares/tokenValidator.ts index 8cb745a9..64b9fe99 100644 --- a/backend/src/middlewares/tokenValidator.ts +++ b/backend/src/middlewares/tokenValidator.ts @@ -1,12 +1,15 @@ import { NextFunction, Request, Response } from 'express'; import { Unauthorized, Message } from '@errors'; +import { catchAsync } from '@utils/catch-async'; import token from '@utils/token'; const guard = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = token.verifyJWT(req.cookies.access_token); + res.locals.user = { id }; + next(); } catch (err) { if (err.message === 'jwt expired') { @@ -28,4 +31,4 @@ const guard = async (req: Request, res: Response, next: NextFunction) => { } }; -export default guard; +export default catchAsync(guard); diff --git a/backend/src/utils/token.ts b/backend/src/utils/token.ts index 2974c234..9f6bb354 100644 --- a/backend/src/utils/token.ts +++ b/backend/src/utils/token.ts @@ -1,18 +1,18 @@ -import jwt, { JwtPayload } from 'jsonwebtoken'; +import { decode, JwtPayload, sign, verify } from 'jsonwebtoken'; import { prisma } from '@config/orm.config'; import { Message, Unauthorized } from '@errors'; const generateJWT = (expiresIn: '3h' | '7d', payload: { id?: number } = {}) => { - return jwt.sign(payload, process.env.JWT_SECRET_KEY, { expiresIn }); + return sign(payload, process.env.JWT_SECRET_KEY, { expiresIn }); }; const verifyJWT = (token: string) => { - return jwt.verify(token, process.env.JWT_SECRET_KEY) as JwtPayload; + return verify(token, process.env.JWT_SECRET_KEY) as JwtPayload; }; const decodeJWT = (token: string) => { - return jwt.decode(token) as JwtPayload; + return decode(token) as JwtPayload; }; const getTokens = (userId: number) => { diff --git a/frontend/apis/articleApi.ts b/frontend/apis/articleApi.ts index fe5f71eb..d817a6ba 100644 --- a/frontend/apis/articleApi.ts +++ b/frontend/apis/articleApi.ts @@ -43,3 +43,40 @@ export const createArticleApi = async (data: CreateArticleApi) => { return response.data; }; + +export const modifyArticleApi = async (articleId: number, data: CreateArticleApi) => { + const url = `/api/articles/${articleId}`; + + const response = await api({ url, method: 'PATCH', data }); + + return response.data; +}; + +export const deleteArticleApi = async (articleId: string) => { + const url = `/api/articles/${articleId}`; + + const response = await api({ url, method: 'DELETE' }); + + return response.data; +}; + +export const getTemporaryArticleApi = async () => { + const url = '/api/articles/temporary'; + + const response = await api({ url, method: 'GET' }); + + return response.data; +}; + +interface CreateTemporaryArticleApi { + title: string; + content: string; +} + +export const createTemporaryArticleApi = async (data: CreateTemporaryArticleApi) => { + const url = '/api/articles/temporary'; + + const response = await api({ url, method: 'POST', data }); + + return response.data; +}; diff --git a/frontend/apis/bookApi.ts b/frontend/apis/bookApi.ts index c2563212..1ccf0e09 100644 --- a/frontend/apis/bookApi.ts +++ b/frontend/apis/bookApi.ts @@ -30,7 +30,7 @@ interface GetBooksApi { interface EditBookApi { id: number; title: string; - thumbnail_image: any; + thumbnail_image: string; scraps: IScrap[]; } diff --git a/frontend/apis/scrapApi.ts b/frontend/apis/scrapApi.ts index 2c79576b..2ac50ba2 100644 --- a/frontend/apis/scrapApi.ts +++ b/frontend/apis/scrapApi.ts @@ -1,5 +1,13 @@ import api from '@utils/api'; +export const getScrapsApi = async () => { + const url = '/api/scraps'; + + const response = await api({ url, method: 'GET' }); + + return response.data; +}; + interface CreateScrapApi { order: number; is_original: boolean; @@ -7,7 +15,6 @@ interface CreateScrapApi { article_id: number; } -// eslint-disable-next-line import/prefer-default-export export const createScrapApi = async (data: CreateScrapApi) => { const url = `/api/scraps`; @@ -15,3 +22,11 @@ export const createScrapApi = async (data: CreateScrapApi) => { return response.data; }; + +export const deleteScrapApi = async (scrapId: string) => { + const url = `/api/scraps/${scrapId}`; + + const response = await api({ url, method: 'DELETE' }); + + return response.data; +}; diff --git a/frontend/atoms/article.ts b/frontend/atoms/article.ts index 7487a1c8..0762b96e 100644 --- a/frontend/atoms/article.ts +++ b/frontend/atoms/article.ts @@ -3,6 +3,7 @@ import { atom } from 'recoil'; const articleState = atom({ key: 'articleState', default: { + id: -1, title: '', content: '', book_id: -1, diff --git a/frontend/atoms/articleBuffer.ts b/frontend/atoms/articleBuffer.ts new file mode 100644 index 00000000..2cee747d --- /dev/null +++ b/frontend/atoms/articleBuffer.ts @@ -0,0 +1,11 @@ +import { atom } from 'recoil'; + +const articleBuffer = atom({ + key: 'articleBuffer', + default: { + title: '', + content: '', + }, +}); + +export default articleBuffer; diff --git a/frontend/atoms/editInfo.ts b/frontend/atoms/editInfo.ts index de56e0df..0d41faa5 100644 --- a/frontend/atoms/editInfo.ts +++ b/frontend/atoms/editInfo.ts @@ -10,6 +10,8 @@ interface EditInfoState { thumbnail_image: string; scraps: IScrap[]; }[]; + deletedArticle: number[]; + deletedScraps: number[]; } const editInfoState = atom({ @@ -17,6 +19,8 @@ const editInfoState = atom({ default: { deleted: [], editted: [], + deletedArticle: [], + deletedScraps: [], }, }); diff --git a/frontend/atoms/scrap.ts b/frontend/atoms/scrap.ts index 4b0ea75c..3ed43b66 100644 --- a/frontend/atoms/scrap.ts +++ b/frontend/atoms/scrap.ts @@ -1,6 +1,8 @@ import { atom } from 'recoil'; -const scrapState = atom({ +import { IScrap } from '@interfaces'; + +const scrapState = atom({ key: 'scrapState', default: [], }); diff --git a/frontend/components/CheckSignInByToken/index.tsx b/frontend/components/auth/CheckSignInStatus/index.tsx similarity index 82% rename from frontend/components/CheckSignInByToken/index.tsx rename to frontend/components/auth/CheckSignInStatus/index.tsx index ffbe1b07..9eaefe80 100644 --- a/frontend/components/CheckSignInByToken/index.tsx +++ b/frontend/components/auth/CheckSignInStatus/index.tsx @@ -6,11 +6,11 @@ import { checkSignInApi } from '@apis/authApi'; import signInStatusState from '@atoms/signInStatus'; import useFetch from '@hooks/useFetch'; -interface CheckSignInByTokenProps { +interface CheckSignInStatus { children: React.ReactNode; } -export default function CheckSignInByToken({ children }: CheckSignInByTokenProps) { +export default function CheckSignInStatus({ children }: CheckSignInStatus) { const { data: user, execute: checkSignIn } = useFetch(checkSignInApi); const setSignInStatus = useSetRecoilState(signInStatusState); diff --git a/frontend/components/SignInModal/index.tsx b/frontend/components/auth/SignInModal/index.tsx similarity index 100% rename from frontend/components/SignInModal/index.tsx rename to frontend/components/auth/SignInModal/index.tsx diff --git a/frontend/components/SignInModal/styled.ts b/frontend/components/auth/SignInModal/styled.ts similarity index 79% rename from frontend/components/SignInModal/styled.ts rename to frontend/components/auth/SignInModal/styled.ts index 27028780..6bd3bb45 100644 --- a/frontend/components/SignInModal/styled.ts +++ b/frontend/components/auth/SignInModal/styled.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { FlexColumnCenter } from '@styles/layout'; +import { FlexColumnAlignCenter } from '@styles/layout'; export const SignInModalWrapper = styled.div` margin-top: 56px; @@ -10,7 +10,7 @@ export const SignInModalWrapper = styled.div` box-sizing: border-box; `; -export const SignUpContainer = styled(FlexColumnCenter)` +export const SignUpContainer = styled(FlexColumnAlignCenter)` padding: 20px; gap: 10px; diff --git a/frontend/components/SignUpModal/index.tsx b/frontend/components/auth/SignUpModal/index.tsx similarity index 100% rename from frontend/components/SignUpModal/index.tsx rename to frontend/components/auth/SignUpModal/index.tsx diff --git a/frontend/components/SignUpModal/styled.ts b/frontend/components/auth/SignUpModal/styled.ts similarity index 100% rename from frontend/components/SignUpModal/styled.ts rename to frontend/components/auth/SignUpModal/styled.ts diff --git a/frontend/components/common/Book/index.tsx b/frontend/components/common/Book/index.tsx index 75aa183b..9e4fc0fb 100644 --- a/frontend/components/common/Book/index.tsx +++ b/frontend/components/common/Book/index.tsx @@ -20,6 +20,7 @@ import { ArticleLink, AuthorLink, BookmarkIcon, + BookLink, } from './styled'; interface BookProps { @@ -36,17 +37,27 @@ export default function Book({ book }: BookProps) { return ( // 수정모드일때만 아래 onclick이 실행되도록 수정해야함 -> 민형님 작업 후 - + + + - {title} + + {title} + by {user.nickname} @@ -74,11 +85,11 @@ export default function Book({ book }: BookProps) { )} - {scraps.length > 4 && ( + {/* {scraps.length > 4 && ( More Contents Icon - )} + )} */} ); diff --git a/frontend/components/common/Book/styled.ts b/frontend/components/common/Book/styled.ts index 9b462b2a..9e564be3 100644 --- a/frontend/components/common/Book/styled.ts +++ b/frontend/components/common/Book/styled.ts @@ -7,8 +7,7 @@ import { TextXSmall } from '@styles/common'; import { FlexColumn } from '@styles/layout'; export const BookWrapper = styled(FlexColumn)` - min-width: 280px; - max-width: 280px; + width: 280px; height: 480px; margin: 0 10px; box-sizing: border-box; @@ -19,12 +18,24 @@ export const BookWrapper = styled(FlexColumn)` overflow: hidden; color: var(--grey-01-color); + aspect-ratio: 280/480; + @media ${(props) => props.theme.tablet} { + width: 100%; + height: auto; + overflow: none; + } `; export const BookThumbnail = styled(Image)` width: 280px; - height: 200px; min-height: 200px; + object-fit: cover; + aspect-ratio: 280/200; + + @media ${(props) => props.theme.tablet} { + width: 100%; + min-height: auto; + } `; export const BookInfoContainer = styled(FlexColumn)` @@ -100,3 +111,8 @@ export const AuthorLink = styled(Link)` display: block; margin-top: 2px; `; + +export const BookLink = styled(Link)<{ isArticleExists: boolean }>` + text-decoration: none; + ${(props) => (props.isArticleExists ? '' : 'pointer-events: none;')} +`; diff --git a/frontend/components/common/DragDrop/Container/index.tsx b/frontend/components/common/DragDrop/Container/index.tsx index f741ef02..e1c50ef8 100644 --- a/frontend/components/common/DragDrop/Container/index.tsx +++ b/frontend/components/common/DragDrop/Container/index.tsx @@ -5,6 +5,7 @@ import update from 'immutability-helper'; import { useRecoilState } from 'recoil'; import scrapState from '@atoms/scrap'; +import { IScrap } from '@interfaces'; import { ListItem } from '../ListItem'; import ContainerWapper from './styled'; @@ -13,21 +14,17 @@ const ItemTypes = { Scrap: 'scrap', }; -export interface EditScrap { - id: number; - order: number; - article: { - id: number; - title: string; - }; -} export interface ContainerState { - data: EditScrap[]; + data: IScrap[]; isContentsShown: boolean; + isDeleteBtnShown: boolean; } - -export const Container = memo(function Container({ data, isContentsShown }: ContainerState) { - const [scraps, setScraps] = useRecoilState(scrapState); +const DragContainer = memo(function Container({ + data, + isContentsShown, + isDeleteBtnShown, +}: ContainerState) { + const [scraps, setScraps] = useRecoilState(scrapState); useEffect(() => { if (!data) return; @@ -35,15 +32,8 @@ export const Container = memo(function Container({ data, isContentsShown }: Cont }, []); const findScrap = useCallback( - (id: string) => { - const scrap = scraps.filter((c) => `${c.article.id}` === id)[0] as { - id: number; - order: number; - article: { - id: number; - title: string; - }; - }; + (id: number) => { + const scrap = scraps.filter((c) => c.article.id === id)[0]; return { scrap, index: scraps.indexOf(scrap), @@ -53,7 +43,7 @@ export const Container = memo(function Container({ data, isContentsShown }: Cont ); const moveScrap = useCallback( - (id: string, atIndex: number) => { + (id: number, atIndex: number) => { const { scrap, index } = findScrap(id); setScraps( update(scraps, { @@ -73,14 +63,19 @@ export const Container = memo(function Container({ data, isContentsShown }: Cont {scraps.map((scrap, index) => ( ))} ); }); + +export default DragContainer; diff --git a/frontend/components/common/DragDrop/Container/styled.ts b/frontend/components/common/DragDrop/Container/styled.ts index 7dbec1b6..8b22b090 100644 --- a/frontend/components/common/DragDrop/Container/styled.ts +++ b/frontend/components/common/DragDrop/Container/styled.ts @@ -4,11 +4,6 @@ const ContainerWapper = styled.div` display: flex; flex-direction: column; width: 100%; - - div { - border-bottom: 1px solid var(--grey-02-color); - height: 28px; - } `; export default ContainerWapper; diff --git a/frontend/components/common/DragDrop/ListItem/index.tsx b/frontend/components/common/DragDrop/ListItem/index.tsx index bdfba4ce..1175f52b 100644 --- a/frontend/components/common/DragDrop/ListItem/index.tsx +++ b/frontend/components/common/DragDrop/ListItem/index.tsx @@ -1,35 +1,49 @@ import { memo } from 'react'; import { useDrag, useDrop } from 'react-dnd'; -import Article from './styled'; +import { useRecoilState } from 'recoil'; + +import MinusWhite from '@assets/ico_minus_white.svg'; +import editInfoState from '@atoms/editInfo'; +import scrapState from '@atoms/scrap'; + +import { Article, Text, MinusButton, MinusIcon, OriginalBadge, TextWapper } from './styled'; const ItemTypes = { Scrap: 'scrap', }; export interface ScrapProps { - id: string; + id: number; + scrapId: number; text: string; - moveScrap: (id: string, to: number) => void; - findScrap: (id: string) => { index: number }; + isOriginal: boolean; + moveScrap: (id: number, to: number) => void; + findScrap: (id: number) => { index: number }; isShown: boolean; isContentsShown: boolean; + isDeleteBtnShown: boolean; } interface Item { - id: string; + id: number; originalIndex: number; } export const ListItem = memo(function Scrap({ id, + scrapId, text, + isOriginal, moveScrap, findScrap, isShown, isContentsShown, + isDeleteBtnShown, }: ScrapProps) { const originalIndex = findScrap(id).index; + const [scraps, setScraps] = useRecoilState(scrapState); + const [editInfo, setEditInfo] = useRecoilState(editInfoState); // Drag const [{ isDragging }, drag] = useDrag( @@ -67,9 +81,39 @@ export const ListItem = memo(function Scrap({ [findScrap, moveScrap] ); + const handleMinusBtnClick = () => { + // 원본글이 아니면 스크랩에서만 삭제 + // 원본글이면 실제로 삭제 + if (window.confirm('글을 책에서 삭제하시겠습니까?')) { + if (isOriginal) { + setEditInfo({ + ...editInfo, + deletedArticle: [...editInfo.deletedArticle, id], + deletedScraps: [...editInfo.deletedScraps, scrapId], + }); + setScraps(scraps.filter((v) => v.article.id !== id)); + return; + } + + setEditInfo({ + ...editInfo, + deletedScraps: [...editInfo.deletedScraps, scrapId], + }); + setScraps(scraps.filter((v) => v.article.id !== id)); + } + }; + return (
drag(drop(node))} isShown={isContentsShown ? true : isShown}> - {text} + + {text} + {isOriginal && isDeleteBtnShown && 원본} + + {isDeleteBtnShown && ( + + + + )}
); }); diff --git a/frontend/components/common/DragDrop/ListItem/styled.ts b/frontend/components/common/DragDrop/ListItem/styled.ts index 746f128a..4e0e27eb 100644 --- a/frontend/components/common/DragDrop/ListItem/styled.ts +++ b/frontend/components/common/DragDrop/ListItem/styled.ts @@ -1,13 +1,39 @@ +import Image from 'next/image'; + import styled from 'styled-components'; -const Article = styled.div<{ isShown: true | false }>` +import { TextXSmall } from '@styles/common'; +import { Flex } from '@styles/layout'; + +export const Text = styled.span``; +export const Article = styled.div<{ isShown: true | false }>` font-size: 14px; line-height: 20px; text-decoration: none; color: inherit; - display: ${(props) => (props.isShown ? 'block' : 'none')}; - padding: 2px 0; + display: ${(props) => (props.isShown ? 'flex' : 'none')}; + justify-content: space-between; + align-items: center; border-bottom: 1px solid var(--grey-02-color); + padding: 5px; +`; + +export const MinusButton = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 30px; + border-radius: 50%; + background-color: var(--red-color); `; -export default Article; +export const MinusIcon = styled(Image)``; +export const TextWapper = styled(Flex)``; + +export const OriginalBadge = styled(TextXSmall)` + // background-color: var(--grey-02-color); + border: 1px solid var(--grey-02-color); + border-radius: 10px; + padding: 1px 3px; + margin-left: 5px; +`; diff --git a/frontend/components/common/DragDrop/dndInterface.ts b/frontend/components/common/DragDrop/dndInterface.ts deleted file mode 100644 index 5cb406a1..00000000 --- a/frontend/components/common/DragDrop/dndInterface.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface EditScrap { - id: number; - order: number; - article: { - id: number; - title: string; - }; -} -export interface ContainerState { - data: EditScrap[]; - isContentsShown: boolean; -} diff --git a/frontend/components/common/DragDrop/index.tsx b/frontend/components/common/DragDrop/index.tsx index fa1ed574..ed4d57d7 100644 --- a/frontend/components/common/DragDrop/index.tsx +++ b/frontend/components/common/DragDrop/index.tsx @@ -1,11 +1,12 @@ import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; +import { TouchBackend } from 'react-dnd-touch-backend'; -import { Container } from '@components/common/DragDrop/Container'; +import DragContainer from '@components/common/DragDrop/Container'; export interface EditScrap { id: number; order: number; + is_original: boolean; article: { id: number; title: string; @@ -14,12 +15,17 @@ export interface EditScrap { export interface ContainerState { data: EditScrap[]; isContentsShown: boolean; + isDeleteBtnShown: boolean; } -export default function DragArticle({ data, isContentsShown }: any) { +export default function DragArticle({ data, isContentsShown, isDeleteBtnShown }: ContainerState) { return ( - - + + ); } diff --git a/frontend/components/common/GNB/index.tsx b/frontend/components/common/GNB/index.tsx index 5d7f591d..9879bdcf 100644 --- a/frontend/components/common/GNB/index.tsx +++ b/frontend/components/common/GNB/index.tsx @@ -8,9 +8,9 @@ import ArticleIcon from '@assets/ico_article.svg'; import PersonIcon from '@assets/ico_person.svg'; import SearchIcon from '@assets/ico_search.svg'; import signInStatusState from '@atoms/signInStatus'; +import SignInModal from '@components/auth/SignInModal'; +import SignUpModal from '@components/auth/SignUpModal'; import Modal from '@components/common/Modal'; -import SignInModal from '@components/SignInModal'; -import SignUpModal from '@components/SignUpModal'; import { GNBbar, Icon, IconsContainer, Logo } from './styled'; diff --git a/frontend/components/common/Modal/styled.ts b/frontend/components/common/Modal/styled.ts index 8025d329..ee46f1d7 100644 --- a/frontend/components/common/Modal/styled.ts +++ b/frontend/components/common/Modal/styled.ts @@ -27,6 +27,11 @@ export const ModalInner = styled.div` background: var(--white-color); border-radius: 30px; z-index: 100; + + @media ${(props) => props.theme.mobile} { + width: 320px; + padding: 32px 20px; + } `; export const ButtonWrapper = styled.div<{ hasBackward?: boolean }>` diff --git a/frontend/components/common/SkeletonBook/index.tsx b/frontend/components/common/SkeletonBook/index.tsx new file mode 100644 index 00000000..23ca92f3 --- /dev/null +++ b/frontend/components/common/SkeletonBook/index.tsx @@ -0,0 +1,38 @@ +import { FlexColumn, FlexSpaceBetween } from '@styles/layout'; + +import { + BookWrapper, + BookInfoContainer, + BookTitle, + BookContentsInfo, + BookContents, + BookThumbnail, + BookAuthor, + Bookmark, +} from './styled'; + +export default function SkeletonBook() { + const bookContentsList = Array.from({ length: 4 }, (_, i) => i + 1); + + return ( + + + + + + + + + + + + + + {bookContentsList.map((key) => ( + + ))} + + + + ); +} diff --git a/frontend/components/common/SkeletonBook/styled.ts b/frontend/components/common/SkeletonBook/styled.ts new file mode 100644 index 00000000..8211d17e --- /dev/null +++ b/frontend/components/common/SkeletonBook/styled.ts @@ -0,0 +1,90 @@ +import Image from 'next/image'; + +import styled from 'styled-components'; + +import { FlexColumn } from '@styles/layout'; + +const SkeletonItem = styled.div` + width: 100%; + height: 30px; + background-color: #f2f2f2; + position: relative; + overflow: hidden; + border-radius: 4px; + + @keyframes skeleton-gradient { + 0% { + background-color: rgba(165, 165, 165, 0.1); + } + 50% { + background-color: rgba(165, 165, 165, 0.3); + } + 100% { + background-color: rgba(165, 165, 165, 0.1); + } + } + + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + animation: skeleton-gradient 1.5s infinite ease-in-out; + } +`; + +export const BookWrapper = styled(FlexColumn)` + min-width: 280px; + max-width: 280px; + height: 480px; + margin: 0 10px; + box-sizing: border-box; + + background-color: var(--white-color); + border: 1px solid var(--primary-color); + border-radius: 10px; + overflow: hidden; +`; + +export const BookThumbnail = styled(SkeletonItem)` + width: 280px; + height: 200px; + min-height: 200px; +`; + +export const BookInfoContainer = styled(FlexColumn)` + padding: 15px 24px; + gap: 18px; +`; + +export const BookTitle = styled(SkeletonItem)` + height: 30px; + width: 140px; +`; + +export const BookAuthor = styled(SkeletonItem)` + height: 20px; + width: 60px; + margin-top: 10px; +`; + +export const Bookmark = styled(SkeletonItem)` + height: 30px; + width: 30px; +`; + +export const BookmarkIcon = styled(Image)` + cursor: pointer; +`; + +export const BookContentsInfo = styled(FlexColumn)` + gap: 8px; + margin-top: 30px; +`; + +export const BookContents = styled(SkeletonItem)` + height: 20px; + width: 100%; +`; diff --git a/frontend/components/common/Spinner/index.tsx b/frontend/components/common/Spinner/index.tsx new file mode 100644 index 00000000..60fa0e24 --- /dev/null +++ b/frontend/components/common/Spinner/index.tsx @@ -0,0 +1,19 @@ +import { SpinnerInner, SpinnerWrapper } from './styled'; + +interface SpinnerStyle { + width: number; + height: number; + borderWidth: number; +} + +interface SpinnerProps { + style: SpinnerStyle; +} + +export default function Spinner({ style }: SpinnerProps) { + return ( + + + + ); +} diff --git a/frontend/components/common/Spinner/styled.ts b/frontend/components/common/Spinner/styled.ts new file mode 100644 index 00000000..668c9ac5 --- /dev/null +++ b/frontend/components/common/Spinner/styled.ts @@ -0,0 +1,37 @@ +import styled from 'styled-components'; + +interface SpinnerWrapperProps { + width: number; + height: number; +} + +export const SpinnerWrapper = styled.div` + width: ${(props) => props.width}px; + height: ${(props) => props.height}px; +`; + +interface SpinnerInnerProps { + borderWidth: number; +} + +export const SpinnerInner = styled.div` + width: 100%; + height: 100%; + background-color: transparent; + border: ${(props) => props.borderWidth}px solid var(--grey-02-color); + border-top: ${(props) => props.borderWidth}px solid var(--primary-color); + border-radius: 50%; + box-sizing: border-box; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } + } + + animation: spin 1s infinite ease; +`; diff --git a/frontend/components/edit/EditBar/index.tsx b/frontend/components/edit/EditBar/index.tsx index 5f8ae692..18127792 100644 --- a/frontend/components/edit/EditBar/index.tsx +++ b/frontend/components/edit/EditBar/index.tsx @@ -1,19 +1,65 @@ +import { useRouter } from 'next/router'; + +import { useEffect } from 'react'; + +import { useRecoilValue, useSetRecoilState } from 'recoil'; + +import { createTemporaryArticleApi, getTemporaryArticleApi } from '@apis/articleApi'; +import articleState from '@atoms/article'; +import articleBuffer from '@atoms/articleBuffer'; +import useFetch from '@hooks/useFetch'; + import { Bar, ButtonGroup, ExitButton, PublishButton, TemporaryButton } from './styled'; interface EditBarProps { handleModalOpen: () => void; + isModifyMode: boolean; } -export default function EditBar({ handleModalOpen }: EditBarProps) { +export default function EditBar({ handleModalOpen, isModifyMode }: EditBarProps) { + const article = useRecoilValue(articleState); + const setBuffer = useSetRecoilState(articleBuffer); + const router = useRouter(); + + const { data: temporaryArticle, execute: getTemporaryArticle } = useFetch(getTemporaryArticleApi); + const { execute: createTemporaryArticle } = useFetch(createTemporaryArticleApi); + + const handleLoadButton = () => { + getTemporaryArticle(); + }; + + const handleSaveButton = () => { + createTemporaryArticle({ title: article.title, content: article.content }); + }; + + const handleExitButton = () => { + const confirm = window.confirm('정말 나가시겠습니까?'); + + if (confirm) router.push('/'); + }; + + useEffect(() => { + if (!temporaryArticle) return; + + setBuffer({ + title: temporaryArticle.title, + content: temporaryArticle.content, + }); + }, [temporaryArticle]); + return ( - 나가기 + handleExitButton()}> + 나가기 + - 불러오기 - 임시 저장 - 발행 + handleLoadButton()}>불러오기 + handleSaveButton()}>저장 + + {isModifyMode ? '수정하기' : '발행'} + ); diff --git a/frontend/components/edit/EditHead/index.tsx b/frontend/components/edit/EditHead/index.tsx new file mode 100644 index 00000000..f5deb269 --- /dev/null +++ b/frontend/components/edit/EditHead/index.tsx @@ -0,0 +1,9 @@ +import Head from 'next/head'; + +export default function EditHead() { + return ( + + Knoticle + + ); +} diff --git a/frontend/components/edit/Editor/core/theme.ts b/frontend/components/edit/Editor/core/theme.ts index 362f83a8..8ba08326 100644 --- a/frontend/components/edit/Editor/core/theme.ts +++ b/frontend/components/edit/Editor/core/theme.ts @@ -3,21 +3,23 @@ import { tags } from '@lezer/highlight'; export default function theme() { const highlightStyle = HighlightStyle.define([ - { - tag: tags.heading1, - 'font-size': '24px', - 'font-weight': '700', - }, - { - tag: tags.heading2, - 'font-size': '20px', - 'font-weight': '700', - }, - { - tag: tags.heading3, - 'font-size': '16px', - 'font-weight': '700', - }, + { tag: tags.heading1, fontSize: '24px', fontWeight: '700' }, + { tag: tags.heading2, fontSize: '20px', fontWeight: '700' }, + { tag: tags.heading3, fontSize: '16px', fontWeight: '700' }, + { tag: tags.link, textDecoration: 'underline' }, + { tag: tags.strikethrough, textDecoration: 'line-through' }, + { tag: tags.invalid, color: '#cb2431' }, + { tag: [tags.string, tags.meta, tags.regexp], color: '#222222', fontWeight: 700 }, + { tag: [tags.heading, tags.strong], color: '#222222', fontWeight: '700' }, + { tag: [tags.emphasis], color: '#24292e', fontStyle: 'italic' }, + { tag: [tags.comment, tags.bracket], color: '#6a737d' }, + { tag: [tags.className, tags.propertyName], color: '#6f42c1' }, + { tag: [tags.variableName, tags.attributeName, tags.number, tags.operator], color: '#005cc5' }, + { tag: [tags.keyword, tags.typeName, tags.typeOperator, tags.typeName], color: '#d73a49' }, + { tag: [tags.name, tags.quote], color: '#22863a' }, + { tag: [tags.deleted], color: '#b31d28', backgroundColor: 'ffeef0' }, + { tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: '#e36209' }, + { tag: [tags.url, tags.escape, tags.regexp, tags.link], color: '#222222' }, ]); return [syntaxHighlighting(highlightStyle)]; diff --git a/frontend/components/edit/Editor/core/useCodeMirror.ts b/frontend/components/edit/Editor/core/useCodeMirror.ts index 832fed35..b2ab2cb4 100644 --- a/frontend/components/edit/Editor/core/useCodeMirror.ts +++ b/frontend/components/edit/Editor/core/useCodeMirror.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; +import { languages } from '@codemirror/language-data'; import { EditorState } from '@codemirror/state'; import { placeholder } from '@codemirror/view'; import { EditorView } from 'codemirror'; @@ -15,7 +16,7 @@ export default function useCodeMirror() { const [editorView, setEditorView] = useState(); - const [value, setValue] = useState(''); + const [document, setDocument] = useState(''); const [element, setElement] = useState(); const ref = useCallback((node: HTMLElement | null) => { @@ -24,9 +25,21 @@ export default function useCodeMirror() { setElement(node); }, []); + const replaceDocument = (insert: string) => { + if (!editorView) return; + + editorView.dispatch({ + changes: { + from: 0, + to: editorView.state.doc.length, + insert, + }, + }); + }; + const onChange = () => { return EditorView.updateListener.of(({ view, docChanged }) => { - if (docChanged) setValue(view.state.doc.toString()); + if (docChanged) setDocument(view.state.doc.toString()); }); }; @@ -60,7 +73,7 @@ export default function useCodeMirror() { const markdownImage = (path: string) => `![image](${path})\n`; - const insert = markdownImage(image.imagePath); + const insert = markdownImage(image?.imagePath); editorView.dispatch({ changes: { @@ -77,11 +90,15 @@ export default function useCodeMirror() { const editorState = EditorState.create({ extensions: [ - markdown({ base: markdownLanguage }), + markdown({ + base: markdownLanguage, + codeLanguages: languages, + }), placeholder('내용을 입력해주세요.'), theme(), onChange(), onPaste(), + EditorView.lineWrapping, ], }); @@ -96,5 +113,5 @@ export default function useCodeMirror() { return () => view?.destroy(); }, [element]); - return { ref, value }; + return { ref, document, replaceDocument }; } diff --git a/frontend/components/edit/Editor/index.tsx b/frontend/components/edit/Editor/index.tsx index 6d1297a8..357b92e5 100644 --- a/frontend/components/edit/Editor/index.tsx +++ b/frontend/components/edit/Editor/index.tsx @@ -1,47 +1,57 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useRecoilState } from 'recoil'; -import rehypeStringify from 'rehype-stringify'; -import remarkParse from 'remark-parse'; -import remarkRehype from 'remark-rehype'; -import { unified } from 'unified'; import articleState from '@atoms/article'; +import articleBuffer from '@atoms/articleBuffer'; import Content from '@components/common/Content'; import EditBar from '@components/edit/EditBar'; import useCodeMirror from '@components/edit/Editor/core/useCodeMirror'; import useInput from '@hooks/useInput'; +import { IArticle } from '@interfaces'; +import { html2markdown, markdown2html } from '@utils/parser'; import { CodeMirrorWrapper, EditorInner, EditorWrapper, TitleInput } from './styled'; interface EditorProps { handleModalOpen: () => void; + originalArticle?: IArticle; } -export default function Editor({ handleModalOpen }: EditorProps) { - const { ref, value } = useCodeMirror(); +export default function Editor({ handleModalOpen, originalArticle }: EditorProps) { + const { ref, document, replaceDocument } = useCodeMirror(); + const [buffer, setBuffer] = useRecoilState(articleBuffer); + const [isModifyMode, setIsModifyMode] = useState(false); const [article, setArticle] = useRecoilState(articleState); const title = useInput(); useEffect(() => { - setArticle({ - ...article, - title: title.value, - }); - }, [title.value]); + if (originalArticle) { + setIsModifyMode(true); + setBuffer({ + title: originalArticle.title, + content: originalArticle.content, + }); + } + }, [originalArticle]); + + useEffect(() => { + if (!buffer.title && !buffer.content) return; + + title.setValue(buffer.title); + replaceDocument(html2markdown(buffer.content)); + + setBuffer({ title: '', content: '' }); + }, [buffer]); useEffect(() => { setArticle({ ...article, - content: unified() - .use(remarkParse) - .use(remarkRehype) - .use(rehypeStringify) - .processSync(value) - .toString(), + title: title.value, + content: markdown2html(document), }); - }, [value]); + }, [title.value, document]); return ( @@ -50,7 +60,7 @@ export default function Editor({ handleModalOpen }: EditorProps) {
- + @@ -58,3 +68,7 @@ export default function Editor({ handleModalOpen }: EditorProps) { ); } + +Editor.defaultProps = { + originalArticle: '', +}; diff --git a/frontend/components/edit/Editor/styled.ts b/frontend/components/edit/Editor/styled.ts index 117dfb5f..2d5c4d8f 100644 --- a/frontend/components/edit/Editor/styled.ts +++ b/frontend/components/edit/Editor/styled.ts @@ -15,6 +15,12 @@ export const EditorInner = styled.div` overflow: auto; padding: 32px; position: relative; + + @media ${(props) => props.theme.tablet} { + &:nth-child(2) { + display: none; + } + } `; export const CodeMirrorWrapper = styled.div` diff --git a/frontend/components/edit/ModifyModal/index.tsx b/frontend/components/edit/ModifyModal/index.tsx new file mode 100644 index 00000000..6c9714dc --- /dev/null +++ b/frontend/components/edit/ModifyModal/index.tsx @@ -0,0 +1,128 @@ +import { useRouter } from 'next/router'; + +import { useEffect, useState } from 'react'; + +import { useRecoilState } from 'recoil'; + +import { modifyArticleApi } from '@apis/articleApi'; +import articleState from '@atoms/article'; +import scrapState from '@atoms/scrap'; +import DragArticle from '@components/common/DragDrop'; +import Dropdown from '@components/common/Dropdown'; +import ModalButton from '@components/common/Modal/ModalButton'; +import useFetch from '@hooks/useFetch'; +import { IArticle, IBook, IBookScraps, IScrap } from '@interfaces'; + +import { ArticleWrapper, Label, ModifyModalWrapper, WarningLabel } from './styled'; + +interface ModifyModalProps { + books: IBookScraps[]; + originalArticle: IArticle; +} + +export default function ModifyModal({ books, originalArticle }: ModifyModalProps) { + const router = useRouter(); + + const { id: originalArticleId, book_id: originalBookId } = originalArticle as IArticle; + + const { data: modifiedArticle, execute: modifyArticle } = useFetch(modifyArticleApi); + + const [article, setArticle] = useRecoilState(articleState); + + const [selectedBookIndex, setSelectedBookIndex] = useState(-1); + const [filteredScraps, setFilteredScraps] = useState([]); + const [scrapList, setScrapList] = useRecoilState(scrapState); + + const [isSelectedBookUnavailable, setSelectedBookUnavailable] = useState(false); + + const createBookDropdownItems = (items: IBook[]) => + items.map((item) => { + return { + id: item.id, + name: item.title, + }; + }); + + const checkArticleExistsInBook = (articleId: number, items: IScrap[]) => { + return items.some((item) => item.article.id === articleId); + }; + + const createScrapDropdownItems = (items: IScrap[]) => { + const itemList = [...items]; + + if (selectedBookIndex !== originalBookId) + itemList.push({ + id: 0, + order: items.length + 1, + is_original: false, + article: { id: 0, title: article.title }, + }); + return itemList; + }; + + useEffect(() => { + if (selectedBookIndex === -1) return; + + const selectedBook = books.find((book) => book.id === selectedBookIndex); + + if ( + !selectedBook || + (selectedBookIndex !== originalBookId && + checkArticleExistsInBook(originalArticleId, selectedBook.scraps)) + ) { + setSelectedBookIndex(-1); + setSelectedBookUnavailable(true); + setFilteredScraps([]); + return; + } + + setSelectedBookUnavailable(false); + setFilteredScraps(selectedBook.scraps); + + setArticle({ + ...article, + book_id: selectedBookIndex, + }); + }, [selectedBookIndex]); + + useEffect(() => { + setScrapList(createScrapDropdownItems(filteredScraps)); + }, [filteredScraps]); + + const handleModifyBtnClick = () => { + const scraps = scrapList.map((v: IScrap, i: number) => ({ ...v, order: i + 1 })); + modifyArticle(originalArticleId, { article, scraps }); + }; + + useEffect(() => { + if (modifiedArticle) router.push('/'); + }, [modifiedArticle]); + + return ( + + + setSelectedBookIndex(id)} + /> + {isSelectedBookUnavailable && ( + 선택하신 책에 본 글이 스크랩되어 있습니다. + )} + {filteredScraps.length !== 0 && ( + + + + + )} + + 수정하기 + + + ); +} diff --git a/frontend/components/edit/ModifyModal/styled.ts b/frontend/components/edit/ModifyModal/styled.ts new file mode 100644 index 00000000..32010994 --- /dev/null +++ b/frontend/components/edit/ModifyModal/styled.ts @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +import { TextLarge, TextMedium } from '@styles/common'; + +export const ModifyModalWrapper = styled.div` + margin-top: 32px; + + > div { + margin-bottom: 16px; + } +`; + +export const Label = styled(TextLarge)``; +export const ArticleWrapper = styled.div` + width: 100%; + height: 300px; + overflow: auto; +`; + +export const WarningLabel = styled(TextMedium)` + color: var(--red-color); +`; diff --git a/frontend/components/edit/PublishModal/index.tsx b/frontend/components/edit/PublishModal/index.tsx index f0504cb3..bb7de33c 100644 --- a/frontend/components/edit/PublishModal/index.tsx +++ b/frontend/components/edit/PublishModal/index.tsx @@ -12,7 +12,6 @@ import Dropdown from '@components/common/Dropdown'; import ModalButton from '@components/common/Modal/ModalButton'; import useFetch from '@hooks/useFetch'; import { IBook, IBookScraps, IScrap } from '@interfaces'; -import { IEditScrap } from 'interfaces/scrap.interface'; import { ArticleWrapper, Label, PublishModalWrapper } from './styled'; @@ -23,14 +22,14 @@ interface PublishModalProps { export default function PublishModal({ books }: PublishModalProps) { const router = useRouter(); - const { execute: createArticle } = useFetch(createArticleApi); + const { data: createdArticle, execute: createArticle } = useFetch(createArticleApi); // 전역으로 관리해야할까? const [article, setArticle] = useRecoilState(articleState); const [selectedBookIndex, setSelectedBookIndex] = useState(-1); const [filteredScraps, setFilteredScraps] = useState([]); - const [scrapList, setScrapList] = useRecoilState(scrapState); + const [scrapList, setScrapList] = useRecoilState(scrapState); const createBookDropdownItems = (items: IBook[]) => items.map((item) => { @@ -40,12 +39,16 @@ export default function PublishModal({ books }: PublishModalProps) { }; }); - const createScrapDropdownItems = (items: IEditScrap[]) => { - // 깔끔하게 리팩토릭 필요 - const itemList = [...items]; - - itemList.push({ id: 0, order: items.length + 1, article: { id: 0, title: article.title } }); - return itemList; + const createScrapDropdownItems = (items: IScrap[]) => { + return [ + ...items, + { + id: 0, + order: items.length + 1, + is_original: true, + article: { id: article.id, title: article.title }, + }, + ]; }; useEffect(() => { @@ -58,17 +61,21 @@ export default function PublishModal({ books }: PublishModalProps) { book_id: selectedBookIndex, }); }, [selectedBookIndex]); + useEffect(() => { setScrapList(createScrapDropdownItems(filteredScraps)); }, [filteredScraps]); const handlePublishBtnClick = () => { - const scraps = scrapList.map((v: IEditScrap, i: number) => ({ ...v, order: i + 1 })); + const scraps = scrapList.map((v, i) => ({ ...v, order: i + 1 })); createArticle({ article, scraps }); - router.push('/'); }; + useEffect(() => { + if (createdArticle) router.push('/'); + }, [createdArticle]); + return ( @@ -82,7 +89,11 @@ export default function PublishModal({ books }: PublishModalProps) { {filteredScraps.length !== 0 && ( - + )} diff --git a/frontend/components/home/HomeHead/index.tsx b/frontend/components/home/HomeHead/index.tsx new file mode 100644 index 00000000..e01c0384 --- /dev/null +++ b/frontend/components/home/HomeHead/index.tsx @@ -0,0 +1,16 @@ +import Head from 'next/head'; + +export default function HomeHead() { + return ( + + Knoticle + + + + + + + + + ); +} diff --git a/frontend/components/home/Slider/index.tsx b/frontend/components/home/Slider/index.tsx index d65f4fe8..8a2b3970 100644 --- a/frontend/components/home/Slider/index.tsx +++ b/frontend/components/home/Slider/index.tsx @@ -6,6 +6,7 @@ import LeftArrowIcon from '@assets/ico_arrow_left.svg'; import RightArrowIcon from '@assets/ico_arrow_right.svg'; import ListIcon from '@assets/ico_flower.svg'; import Book from '@components/common/Book'; +import SkeletonBook from '@components/common/SkeletonBook'; import { IBookScraps } from '@interfaces'; import { @@ -18,19 +19,24 @@ import { SliderBookContainer, SliderInfoContainer, SliderIcon, + SliderTrack, + SliderBookWrapper, } from './styled'; interface SliderProps { bookList: IBookScraps[]; title: string; + isLoading: boolean; + numberPerPage: number; } -function Slider({ bookList, title }: SliderProps) { +function Slider({ bookList, title, isLoading, numberPerPage }: SliderProps) { const [curBookIndex, setCurBookIndex] = useState(0); const [sliderNumber, setSliderNumber] = useState(1); - const numberPerPage = 4; - const sliderIndicatorCount = Math.ceil(bookList.length / numberPerPage); + const SkeletonList = Array.from({ length: numberPerPage }, (_, i) => i + 1); + + const sliderIndicatorCount = bookList ? Math.ceil(bookList.length / numberPerPage) : 0; const sliderIndicatorNumbersList = Array.from({ length: sliderIndicatorCount }, (_, i) => i + 1); const handleLeftArrowClick = () => { @@ -51,23 +57,31 @@ function Slider({ bookList, title }: SliderProps) { isvisible={(sliderNumber !== 1).toString()} /> - + List Icon {title} - - {sliderIndicatorNumbersList.map((number) => { - return ; - })} - + {numberPerPage !== 1 && ( + + {sliderIndicatorNumbersList.map((number) => { + return ; + })} + + )} - - {bookList.map((book) => ( - - ))} + + + {isLoading + ? SkeletonList.map((key) => ) + : bookList.map((book) => ( + + + + ))} + diff --git a/frontend/components/home/Slider/styled.ts b/frontend/components/home/Slider/styled.ts index f6d5cf98..aed12aa2 100644 --- a/frontend/components/home/Slider/styled.ts +++ b/frontend/components/home/Slider/styled.ts @@ -10,11 +10,18 @@ export const SliderWrapper = styled.div` gap: 10px; `; -export const SliderContent = styled(FlexColumn)` +export const SliderContent = styled(FlexColumn)<{ numberPerPage: number }>` max-width: 1200px; overflow: hidden; gap: 10px; margin-top: 30px; + + max-width: ${(props) => { + if (props.numberPerPage === 1) return '300px'; + if (props.numberPerPage === 2) return '600px'; + if (props.numberPerPage === 3) return '900px'; + return '1200px'; + }}; `; export const SliderInfoContainer = styled(FlexSpaceBetween)` @@ -32,11 +39,14 @@ export const SliderTitle = styled.div` font-weight: 700; `; -export const SliderBookContainer = styled.div<{ curBookIndex: number }>` +export const SliderBookContainer = styled.div` + position: relative; +`; + +export const SliderTrack = styled.div<{ curBookIndex: number }>` display: flex; ${(props) => `transform: translateX(-${300 * props.curBookIndex}px);`} transition: transform 700ms ease 0ms; - z-index: 0; `; export const SliderIndicatorContainer = styled.div` @@ -45,6 +55,29 @@ export const SliderIndicatorContainer = styled.div` gap: 4px; `; +export const SliderBookWrapper = styled.div<{ numberPerPage: number }>` + min-width: 300px; + @media ${(props) => props.theme.desktop} { + min-width: 300px; + } + + @media ${(props) => props.theme.tablet} { + min-width: 280px; + margin: 0 10px; + } + + @media ${(props) => props.theme.mobile} { + min-width: 280px; + margin: 0 10px; + } + + ${(props) => { + if (props.numberPerPage === 1) return 'min-width: 280px; margin: 0 10px;'; + if (props.numberPerPage === 2) return 'min-width: 280px; margin: 0 10px;'; + return 'min-width: 300px;'; + }}; +`; + export const SliderIndicator = styled.div<{ isActive: boolean }>` width: 40px; height: 8px; diff --git a/frontend/components/home/Slider/tempBookData.ts b/frontend/components/home/Slider/tempBookData.ts deleted file mode 100644 index 98f78d44..00000000 --- a/frontend/components/home/Slider/tempBookData.ts +++ /dev/null @@ -1,66 +0,0 @@ -const tempBookDatas = [ - { - id: 1, - title: '리액트 완전 정복', - user: { - id: 1, - nickname: 'MinHK4', - profile_image: '', - }, - scraps: [ - { - order: 1, - article: { - id: 5, - title: 'Create-react-app', - }, - }, - { - order: 2, - article: { - id: 6, - title: 'JSX', - }, - }, - ], - _count: { - bookmarks: 398, - }, - bookmark: [ - { - id: 1, - }, - ], - }, - { - id: 2, - title: '리액트 완전 정복', - user: { - id: 1, - nickname: 'MinHK4', - profile_image: '', - }, - scraps: [ - { - order: 1, - article: { - id: 5, - title: 'Create-react-app', - }, - }, - { - order: 2, - article: { - id: 6, - title: 'JSX', - }, - }, - ], - _count: { - bookmarks: 398, - }, - bookmark: [], - }, -]; - -export default tempBookDatas; diff --git a/frontend/components/search/ArticleItem/styled.ts b/frontend/components/search/ArticleItem/styled.ts index b3eb30da..d59040dc 100644 --- a/frontend/components/search/ArticleItem/styled.ts +++ b/frontend/components/search/ArticleItem/styled.ts @@ -7,6 +7,8 @@ export const ItemWrapper = styled.div` display: flex; gap: 32px; border-bottom: 1px solid var(--grey-02-color); + width: 100%; + box-sizing: border-box; `; export const ItemGroup = styled.div` diff --git a/frontend/components/search/BookList/styled.ts b/frontend/components/search/BookList/styled.ts index 391cf947..19f56885 100644 --- a/frontend/components/search/BookList/styled.ts +++ b/frontend/components/search/BookList/styled.ts @@ -9,7 +9,25 @@ export const BookListWrapper = styled.div` display: grid; grid-template-columns: repeat(3, 1fr); - grid-row-gap: 30px; + box-sizing: border-box; + grid-gap: 20px 10px; + padding: 20px 0px; margin-bottom: 30px; + + & div { + justify-self: center; + } + + @media ${(props) => props.theme.desktop} { + grid-template-columns: repeat(3, 1fr); + } + + @media ${(props) => props.theme.tablet} { + grid-template-columns: repeat(2, 1fr); + } + + @media ${(props) => props.theme.mobile} { + grid-template-columns: repeat(1, 1fr); + } `; diff --git a/frontend/components/search/SearchBar/styled.ts b/frontend/components/search/SearchBar/styled.ts index 860acffb..d8b90b0b 100644 --- a/frontend/components/search/SearchBar/styled.ts +++ b/frontend/components/search/SearchBar/styled.ts @@ -16,4 +16,5 @@ export const SearchBarInput = styled.input` outline: none; font-size: 32px; font-family: Noto Sans KR; + width: 100%; `; diff --git a/frontend/components/search/SearchFilter/index.tsx b/frontend/components/search/SearchFilter/index.tsx index 1a581b73..28a7cf13 100644 --- a/frontend/components/search/SearchFilter/index.tsx +++ b/frontend/components/search/SearchFilter/index.tsx @@ -28,13 +28,15 @@ export default function SearchFilter({ handleFilter }: SearchFilterProps) { 책 - - handleFilter({ userId: e.target.checked ? signInStatus.id : 0 })} - /> - 내 책에서 검색 - + {signInStatus.id !== 0 && ( + + handleFilter({ userId: e.target.checked ? signInStatus.id : 0 })} + /> + 내 책에서 검색 + + )} ); } diff --git a/frontend/components/search/SearchHead/index.tsx b/frontend/components/search/SearchHead/index.tsx new file mode 100644 index 00000000..3ea5bebf --- /dev/null +++ b/frontend/components/search/SearchHead/index.tsx @@ -0,0 +1,9 @@ +import Head from 'next/head'; + +export default function SearchHead() { + return ( + + Knoticle + + ); +} diff --git a/frontend/components/study/AddBook/index.tsx b/frontend/components/study/AddBook/index.tsx index 6f2c88d2..7602bd88 100644 --- a/frontend/components/study/AddBook/index.tsx +++ b/frontend/components/study/AddBook/index.tsx @@ -9,7 +9,6 @@ import signInStatusState from '@atoms/signInStatus'; import Button from '@components/common/Modal/ModalButton'; import useFetch from '@hooks/useFetch'; import useInput from '@hooks/useInput'; -import { IBookScraps } from '@interfaces'; import { FlexSpaceBetween } from '@styles/layout'; import { toastSuccess } from '@utils/toast'; @@ -31,8 +30,7 @@ interface AddBookProps { } export default function AddBook({ handleModalClose }: AddBookProps) { - const [curKnottedBookList, setCurKnottedBookList] = - useRecoilState(curKnottedBookListState); + const [curKnottedBookList, setCurKnottedBookList] = useRecoilState(curKnottedBookListState); const user = useRecoilValue(signInStatusState); const title = useInput(''); const { data: addBookData, execute: addBook } = useFetch(addBookApi); @@ -41,7 +39,7 @@ export default function AddBook({ handleModalClose }: AddBookProps) { if (!addBookData) return; setCurKnottedBookList([...curKnottedBookList, addBookData]); handleModalClose(); - toastSuccess(`${addBookData.title}책이 추가되었습니다!`); + toastSuccess(`<${addBookData.title}>책이 생성되었습니다!`); }, [addBookData]); const handleAddBookBtnClick = () => { diff --git a/frontend/components/study/BookListTab/index.tsx b/frontend/components/study/BookListTab/index.tsx index 281a1571..f659d3a6 100644 --- a/frontend/components/study/BookListTab/index.tsx +++ b/frontend/components/study/BookListTab/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useRecoilState } from 'recoil'; @@ -43,11 +43,11 @@ export default function BookListTab({ const [isEditing, setIsEditing] = useState(false); const handleEditBookModalOpen = (id: number) => { - const curbook = knottedBookList?.find((v) => v.id === id); - if (!curbook) return; + const curBook = knottedBookList?.find((v) => v.id === id); + if (!curBook) return; setModalShown(true); - setCurEditBook(curbook); + setCurEditBook(curBook); }; const handleModalClose = () => { @@ -55,16 +55,25 @@ export default function BookListTab({ }; const handleMinusBtnClick = (e: React.MouseEvent, id: number) => { - e.stopPropagation(); - setCurKnottedBookList([...curKnottedBookList.filter((book) => id !== book.id)]); - setEditInfo({ - ...editInfo, - deleted: [...editInfo.deleted, id], + const curBook = knottedBookList.find((book) => book.id === id); + if (!curBook) return; + const originalArticleList: number[] = []; + + curBook.scraps.forEach((scrap) => { + if (scrap.is_original) originalArticleList.push(scrap.article.id); }); + + if (window.confirm('이 책에는 원본글이 포함되어 있습니다. 정말로 삭제하시겠습니까?')) { + setCurKnottedBookList([...curKnottedBookList.filter((book) => id !== book.id)]); + setEditInfo({ + ...editInfo, + deleted: [...editInfo.deleted, id], + deletedArticle: [...editInfo.deletedArticle, ...originalArticleList], + }); + } }; const handleEditModalOpenerClick = (e: React.MouseEvent, bookId: number) => { - e.stopPropagation(); handleEditBookModalOpen(bookId); }; diff --git a/frontend/components/study/BookListTab/styled.ts b/frontend/components/study/BookListTab/styled.ts index 725c48f9..f4d7a920 100644 --- a/frontend/components/study/BookListTab/styled.ts +++ b/frontend/components/study/BookListTab/styled.ts @@ -24,8 +24,22 @@ export const TabTitleContent = styled(TextLinkMedium)<{ isActive: boolean }>` export const BookGrid = styled.div` display: grid; grid-template-columns: repeat(4, 1fr); - gap: 30px; + box-sizing: border-box; + gap: 20px 0; padding: 20px; + + @media ${(props) => props.theme.desktop} { + grid-template-columns: repeat(3, 1fr); + } + + @media ${(props) => props.theme.tablet} { + grid-template-columns: repeat(2, 1fr); + gap: 20px; + } + + @media ${(props) => props.theme.mobile} { + grid-template-columns: repeat(1, 1fr); + } `; export const EditModeIndicator = styled(TextLinkMedium)` diff --git a/frontend/components/study/EditBook/index.tsx b/frontend/components/study/EditBook/index.tsx index 35b6a5d6..79cccf24 100644 --- a/frontend/components/study/EditBook/index.tsx +++ b/frontend/components/study/EditBook/index.tsx @@ -16,7 +16,6 @@ import useFetch from '@hooks/useFetch'; import useInput from '@hooks/useInput'; import { IBookScraps } from '@interfaces'; import { FlexSpaceBetween } from '@styles/layout'; -import { IEditScrap } from 'interfaces/scrap.interface'; import { BookWrapper, @@ -47,7 +46,7 @@ export default function EditBook({ book, handleModalClose }: BookProps) { const [editInfo, setEditInfo] = useRecoilState(editInfoState); const [curKnottedBookList, setCurKnottedBookList] = useRecoilState(curKnottedBookListState); - const [scrapList] = useRecoilState(scrapState); + const [scrapList] = useRecoilState(scrapState); const [isContentsShown, setIsContentsShown] = useState(false); @@ -70,7 +69,7 @@ export default function EditBook({ book, handleModalClose }: BookProps) { }; const handleCompletedBtnClick = () => { - const editScraps = scrapList.map((v: IEditScrap, i: number) => ({ ...v, order: i + 1 })); + const editScraps = scrapList.map((v, i) => ({ ...v, order: i + 1 })); // 해당하는 책을 찾아서 전역에서 관리하고 있는 애를 변경해서 업데이트 setCurKnottedBookList([ @@ -91,7 +90,7 @@ export default function EditBook({ book, handleModalClose }: BookProps) { setEditInfo({ ...editInfo, editted: [ - ...editInfo.editted, + ...editInfo.editted.filter((edit) => edit.id !== id), { id, title: titleData, @@ -144,7 +143,7 @@ export default function EditBook({ book, handleModalClose }: BookProps) { Contents - + diff --git a/frontend/components/study/EditBook/styled.ts b/frontend/components/study/EditBook/styled.ts index dff242f5..61aa7f40 100644 --- a/frontend/components/study/EditBook/styled.ts +++ b/frontend/components/study/EditBook/styled.ts @@ -3,11 +3,15 @@ import Image from 'next/image'; import styled from 'styled-components'; import { TextXSmall, TextSmall } from '@styles/common'; -import { FlexCenter, FlexColumn, FlexColumnCenter } from '@styles/layout'; +import { FlexCenter, FlexColumn, FlexColumnAlignCenter } from '@styles/layout'; -export const EditBookWapper = styled(FlexColumnCenter)` +export const EditBookWapper = styled(FlexColumnAlignCenter)` width: 320px; margin: auto; + + @media ${(props) => props.theme.mobile} { + width: 300px; + } `; export const BookWrapper = styled(FlexColumn)` @@ -111,6 +115,6 @@ export const MoreContentsIconWrapper = styled(FlexCenter)``; export const DragArticleWrapper = styled.div<{ isContentsShown: true | false }>` ${(props) => props.isContentsShown - ? { overflow: 'auto', height: '400px' } + ? { overflow: 'auto', height: '415px' } : { overflow: 'none', height: '120px' }}; `; diff --git a/frontend/components/study/EditUserProfile/index.tsx b/frontend/components/study/EditUserProfile/index.tsx index 533984e5..0018dcfb 100644 --- a/frontend/components/study/EditUserProfile/index.tsx +++ b/frontend/components/study/EditUserProfile/index.tsx @@ -19,6 +19,8 @@ import { UserThumbnail, EditThumbnailIcon, UserThumbnailGroup, + RedNotice, + UsernameGroup, } from './styled'; interface EditUserProfileProps { @@ -91,15 +93,22 @@ export default function EditUserProfile({ - + + + {nicknameValue === '' && 빈 공백은 닉네임으로 설정할 수 없습니다} + - - 수정 완료 + + {nicknameValue === '' ? '수정 불가' : '수정 완료'} diff --git a/frontend/components/study/EditUserProfile/styled.ts b/frontend/components/study/EditUserProfile/styled.ts index c10293c3..f2c6260e 100644 --- a/frontend/components/study/EditUserProfile/styled.ts +++ b/frontend/components/study/EditUserProfile/styled.ts @@ -2,12 +2,19 @@ import Image from 'next/image'; import styled from 'styled-components'; +import { TextSmall } from '@styles/common'; +import { Flex } from '@styles/layout'; + export const UserProfileWrapper = styled.div` - width: 100%; - margin: 40px 0 20px; + margin: 40px 0 20px 0; + width: 78%; display: flex; - align-items: flex-end; - /* justify-content: flex-start; */ + @media ${(props) => props.theme.mobile} { + flex-direction: column; + justify-content: center; + align-items: center; + margin: 20px; + } `; export const UserThumbnailGroup = styled.div``; @@ -23,7 +30,9 @@ export const UserDetailGroup = styled.div` display: flex; flex-direction: column; margin-left: 30px; - /* background-color: red; */ + @media ${(props) => props.theme.mobile} { + margin-top: 20px; + } `; export const Input = styled.input` @@ -40,6 +49,10 @@ export const Input = styled.input` margin: 5px 0; `; +export const UsernameGroup = styled(Flex)` + align-items: center; +`; + export const EditUsername = styled(Input)` font-size: 18px; line-height: 24px; @@ -49,7 +62,10 @@ export const EditUsername = styled(Input)` export const EditUserDescription = styled(Input)` font-size: 14px; line-height: 20px; - width: 340px; + width: 400px; + @media ${(props) => props.theme.tablet} { + width: 250px; + } `; export const ButtonGroup = styled.div<{ isVisible: boolean }>` @@ -66,16 +82,16 @@ const Button = styled.button` `; export const ProfileEditButton = styled(Button)` - padding: 0 10px; - height: 40px; display: flex; justify-content: space-between; align-items: center; + padding: 0 10px; + height: 40px; border-radius: 10px; - border: 1px solid rgba(148, 173, 46, 1); - background-color: var(--green-color); color: var(--white-color); + border: 1px solid ${(props) => (props.disabled ? 'var(--red-color)' : 'rgba(148, 173, 46, 1)')}; + background-color: ${(props) => (props.disabled ? 'var(--red-color)' : 'var(--green-color)')}; `; export const EditThumbnailIcon = styled.div` @@ -91,3 +107,8 @@ export const EditThumbnailIcon = styled.div` background-color: var(--light-yellow-color); cursor: pointer; `; + +export const RedNotice = styled(TextSmall)` + color: var(--red-color); + margin-left: 10px; +`; diff --git a/frontend/components/study/FAB/index.tsx b/frontend/components/study/FAB/index.tsx index 4e29a301..c89a46c5 100644 --- a/frontend/components/study/FAB/index.tsx +++ b/frontend/components/study/FAB/index.tsx @@ -4,7 +4,9 @@ import { useEffect, useState } from 'react'; import { useRecoilState } from 'recoil'; +import { deleteArticleApi } from '@apis/articleApi'; import { deleteBookApi, editBookApi } from '@apis/bookApi'; +import { deleteScrapApi } from '@apis/scrapApi'; import Add from '@assets/ico_add.svg'; import CheckWhite from '@assets/ico_check_white.svg'; import EditWhite from '@assets/ico_edit_white.svg'; @@ -24,6 +26,8 @@ interface FabProps { export default function FAB({ isEditing, setIsEditing }: FabProps) { const { data: deletedBook, execute: deleteBook } = useFetch(deleteBookApi); const { data: editBookData, execute: editBook } = useFetch(editBookApi); + const { data: deleteArticleData, execute: deleteArticle } = useFetch(deleteArticleApi); + const { data: deleteScrapData, execute: deleteScrap } = useFetch(deleteScrapApi); const [editInfo, setEditInfo] = useRecoilState(editInfoState); @@ -44,6 +48,14 @@ export default function FAB({ isEditing, setIsEditing }: FabProps) { editInfo.editted.forEach((edit) => { editBook(edit); }); + // 원본글 삭제 + editInfo.deletedArticle.forEach((articleId) => { + deleteArticle(articleId); + }); + // 스크랩 삭제 + editInfo.deletedScraps.forEach((scrapId) => { + deleteScrap(scrapId); + }); }; useEffect(() => { @@ -64,15 +76,15 @@ export default function FAB({ isEditing, setIsEditing }: FabProps) { }); }, [editBookData]); - // useEffect(() => { - // if ( - // deletedBook && - // editInfo.deleted.length === 0 && - // editBookData && - // editInfo.editted.length === 0 - // ) - // toastSuccess(`수정 완료되었습니다`); - // }, [deletedBook, editBookData, editInfo]); + useEffect(() => { + if ( + (deletedBook || editBookData) && + editInfo.deleted.length === 0 && + editInfo.editted.length === 0 + ) { + toastSuccess(`수정 완료되었습니다`); + } + }, [editInfo]); return ( diff --git a/frontend/components/study/StudyHead/index.tsx b/frontend/components/study/StudyHead/index.tsx new file mode 100644 index 00000000..720608ba --- /dev/null +++ b/frontend/components/study/StudyHead/index.tsx @@ -0,0 +1,22 @@ +import Head from 'next/head'; + +interface StudyHeadProps { + userNickname: string; + userDescription: string; + userImage: string; +} + +export default function StudyHead({ userNickname, userDescription, userImage }: StudyHeadProps) { + return ( + + {`${userNickname} - Knoticle`} + + + + + + + + + ); +} diff --git a/frontend/components/study/UserProfile/styled.ts b/frontend/components/study/UserProfile/styled.ts index 97df613d..2b89c6bf 100644 --- a/frontend/components/study/UserProfile/styled.ts +++ b/frontend/components/study/UserProfile/styled.ts @@ -5,11 +5,15 @@ import styled from 'styled-components'; import { TextLarge, TextSmall } from '@styles/common'; export const UserProfileWrapper = styled.div` - width: 100%; - margin: 40px 0 20px; + margin: 40px 0 20px 0; + width: 78%; display: flex; - align-items: flex-end; - /* justify-content: flex-start; */ + @media ${(props) => props.theme.mobile} { + flex-direction: column; + justify-content: center; + align-items: center; + margin: 20px; + } `; export const UserThumbnail = styled(Image)` @@ -23,12 +27,22 @@ export const UserDetailGroup = styled.div` display: flex; flex-direction: column; margin-left: 30px; - /* background-color: red; */ + @media ${(props) => props.theme.mobile} { + margin-top: 20px; + } `; -export const Username = styled(TextLarge)``; +export const Username = styled(TextLarge)` + font-size: 24px; + margin-bottom: 10px; +`; -export const UserDescription = styled(TextSmall)``; +export const UserDescription = styled(TextSmall)` + width: 400px; + @media ${(props) => props.theme.tablet} { + width: 300px; + } +`; export const ButtonGroup = styled.div<{ isVisible: boolean }>` display: flex; diff --git a/frontend/components/viewer/ArticleContent/Button/styled.ts b/frontend/components/viewer/ArticleContent/Button/styled.ts index 821193dd..fc811df4 100644 --- a/frontend/components/viewer/ArticleContent/Button/styled.ts +++ b/frontend/components/viewer/ArticleContent/Button/styled.ts @@ -7,7 +7,7 @@ export const ViewerButton = styled.button` border-radius: 10px; border: 1px solid var(--grey-02-color); background-color: white; - margin-left: 10px; + width: fit-content; `; export const ViewerLabel = styled(TextSmall)` diff --git a/frontend/components/viewer/ArticleContent/index.tsx b/frontend/components/viewer/ArticleContent/index.tsx index 2ffca8c9..7536c14d 100644 --- a/frontend/components/viewer/ArticleContent/index.tsx +++ b/frontend/components/viewer/ArticleContent/index.tsx @@ -3,13 +3,16 @@ import { useRouter } from 'next/router'; import { useEffect } from 'react'; -import axios from 'axios'; +import { useRecoilValue } from 'recoil'; +import { deleteArticleApi } from '@apis/articleApi'; import LeftBtnIcon from '@assets/ico_leftBtn.svg'; import Original from '@assets/ico_original.svg'; import RightBtnIcon from '@assets/ico_rightBtn.svg'; import Scrap from '@assets/ico_scrap.svg'; +import signInStatusState from '@atoms/signInStatus'; import Content from '@components/common/Content'; +import useFetch from '@hooks/useFetch'; import { IArticleBook, IScrap } from '@interfaces'; import { TextLarge } from '@styles/common'; @@ -21,100 +24,112 @@ import { ArticleRightBtn, ArticleTitle, ArticleTitleBtnBox, + ArticleContentsWrapper, } from './styled'; interface ArticleProps { article: IArticleBook; scraps: IScrap[]; bookId: number; + bookAuthor: string; handleScrapBtnClick: () => void; } -const user = { - id: 1, - nickname: 'moc1ha', -}; +export default function Article({ + article, + scraps, + bookId, + bookAuthor, + handleScrapBtnClick, +}: ArticleProps) { + const user = useRecoilValue(signInStatusState); -export default function Article({ article, scraps, bookId, handleScrapBtnClick }: ArticleProps) { + const { data: deleteArticleData, execute: deleteArticle } = useFetch(deleteArticleApi); const router = useRouter(); + const handleOriginalBtnOnClick = () => { router.push(`/viewer/${article.book_id}/${article.id}`); }; + const handleLeftBtnOnClick = () => { - const prevOrder = - scraps.filter((scrap: IScrap) => scrap.article.id === article.id)[0].order - 1; - const prevArticleId = scraps.filter((scrap: IScrap) => scrap.order === prevOrder)[0].article.id; + const prevOrder = scraps.filter((scrap) => scrap.article.id === article.id)[0].order - 1; + const prevArticleId = scraps.filter((scrap) => scrap.order === prevOrder)[0].article.id; router.push(`/viewer/${bookId}/${prevArticleId}`); }; + const handleRightBtnOnClick = () => { - const nextOrder = - scraps.filter((scrap: IScrap) => scrap.article.id === article.id)[0].order + 1; - const nextArticleId = scraps.filter((scrap: IScrap) => scrap.order === nextOrder)[0].article.id; + const nextOrder = scraps.filter((scrap) => scrap.article.id === article.id)[0].order + 1; + const nextArticleId = scraps.filter((scrap) => scrap.order === nextOrder)[0].article.id; router.push(`/viewer/${bookId}/${nextArticleId}`); }; + const handleDeleteBtnOnClick = () => { if (window.confirm('해당 글을 삭제하시겠습니까?')) { - axios - .delete(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/articles/${article.id}`) - .catch((err) => { - // 추후 에러 핸들링 추가 예정 - console.log(err); - }); - - router.push('/'); + deleteArticle(article.id); } }; - const checkArticleAuthority = (id: number) => { - if (scraps.find((v: IScrap) => v.article.id === id)) { - return true; + const handleScrapDeleteBtnOnClick = () => { + if (window.confirm('해당 글을 책에서 삭제하시겠습니까?')) { + // } - // alert 두번뜨는 현상... - // 404 페이지로 처리? 고민 중 - // alert('잘못된 접근입니다.'); - router.push('/'); - return false; + }; + + const handleModifyBtnOnClick = () => { + router.push(`/editor?id=${article.id}`); }; useEffect(() => { - checkArticleAuthority(article.id); - }, []); + if (deleteArticleData !== undefined) router.push('/'); + }, [deleteArticleData]); return ( {article.id === scraps.at(0)?.article.id ? null : ( - Viewer Icon + Left Arrow Icon )} {!article.deleted_at ? ( - - {/* Global style Large의 크기가 너무 작음 -> 월요일 회의 후 반영 */} - {article.title} - - {article.book.user.nickname === user.nickname ? ( - 삭제 - ) : ( - - Original Icon - 원본 글 보기 - - )} + + + {article.title} + + + + + + {article.book_id !== bookId && ( + + Original Icon + 원본 글 보기 + + )} + {article.book_id === bookId && article.book.user.nickname === user.nickname && ( + <> + 글 삭제 + 글 수정 + + )} + {/* {article.book_id !== bookId && bookAuthor === user.nickname && ( + 스크랩 삭제 + )} */} + {user.id !== 0 && ( Scrap Icon 스크랩 - - - + )} + ) : ( 삭제된 글입니다. )} + {article.id === scraps.at(-1)?.article.id ? null : ( - Viewer Icon + Right Arrow Icon )} diff --git a/frontend/components/viewer/ArticleContent/styled.ts b/frontend/components/viewer/ArticleContent/styled.ts index 7ccc2807..739a0980 100644 --- a/frontend/components/viewer/ArticleContent/styled.ts +++ b/frontend/components/viewer/ArticleContent/styled.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { Flex } from '@styles/layout'; +import { Flex, FlexColumn } from '@styles/layout'; export const ArticleContainer = styled(Flex)` flex: 1; @@ -11,27 +11,56 @@ export const ArticleLeftBtn = styled.div` top: 50%; margin-left: 20px; cursor: pointer; + + @media ${(props) => props.theme.mobile} { + top: 97px; + } `; export const ArticleRightBtn = styled.div` position: fixed; top: 50%; right: 25px; cursor: pointer; + + @media ${(props) => props.theme.mobile} { + top: 97px; + } `; export const ArticleMain = styled(Flex)` flex-direction: column; width: 100%; padding: 50px; - overflow: auto; + overflow-y: scroll; + box-sizing: border-box; + + ::-webkit-scrollbar { + width: 10px; + } + ::-webkit-scrollbar-thumb { + background-color: var(--grey-02-color); + border-radius: 10px; + } + + @media ${(props) => props.theme.mobile} { + padding: 50px 16px; + } `; -export const ArticleTitle = styled(Flex)` +export const ArticleTitle = styled.div` width: 100%; border-bottom: 1px solid black; padding: 25px 0; - justify-content: space-between; + text-align: left; +`; +export const ArticleTitleBtnBox = styled(Flex)` + flex-wrap: wrap; + gap: 8px; + margin-top: 16px; + border-top: 1px solid var(--grey-02-color); + padding-top: 10px; `; -export const ArticleTitleBtnBox = styled(Flex)``; export const ArticleContents = styled.div` margin-top: 20px; height: 481px; `; + +export const ArticleContentsWrapper = styled(FlexColumn)``; diff --git a/frontend/components/viewer/ClosedSideBar/styled.ts b/frontend/components/viewer/ClosedSideBar/styled.ts index 67b4925b..1ff3f7a8 100644 --- a/frontend/components/viewer/ClosedSideBar/styled.ts +++ b/frontend/components/viewer/ClosedSideBar/styled.ts @@ -1,9 +1,9 @@ -import styled, { keyframes } from 'styled-components'; +import styled from 'styled-components'; import { FlexCenter } from '@styles/layout'; export const ClosedSideBarWrapper = styled.div` - min-width: 50px; + min-width: 30px; height: calc(100vh - 67px); background-color: var(--primary-color); `; diff --git a/frontend/components/viewer/ScrapModal/index.tsx b/frontend/components/viewer/ScrapModal/index.tsx index dcf0125b..88e860fc 100644 --- a/frontend/components/viewer/ScrapModal/index.tsx +++ b/frontend/components/viewer/ScrapModal/index.tsx @@ -8,10 +8,9 @@ import DragArticle from '@components/common/DragDrop'; import Dropdown from '@components/common/Dropdown'; import ModalButton from '@components/common/Modal/ModalButton'; import useFetch from '@hooks/useFetch'; -import { IBook, IBookScraps, IScrap, IArticle } from '@interfaces'; -import { IEditScrap } from 'interfaces/scrap.interface'; +import { IBook, IArticle, IScrap, IBookScraps } from '@interfaces'; -import { ArticleWrapper, Label, ScrapModalWrapper } from './styled'; +import { ArticleWrapper, Label, ScrapModalWrapper, WarningLabel } from './styled'; interface ScrapModalProps { books: IBookScraps[]; @@ -24,7 +23,9 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo const [filteredScraps, setFilteredScraps] = useState([]); const { execute: createScrap } = useFetch(createScrapApi); - const [scrapList, setScrapList] = useRecoilState(scrapState); + const [scrapList, setScrapList] = useRecoilState(scrapState); + + const [isSelectedBookUnavailable, setSelectedBookUnavailable] = useState(false); const createBookDropdownItems = (items: IBook[]) => items.map((item) => { @@ -34,20 +35,37 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo }; }); - const createScrapDropdownItems = (items: IEditScrap[]) => { - const itemList = [...items]; + const createScrapDropdownItems = (items: IScrap[]) => { + return [ + ...items, + { + id: 0, + order: items.length + 1, + is_original: true, + article: { id: article.id, title: article.title }, + }, + ]; + }; - itemList.push({ - id: 0, - order: items.length + 1, - article: { id: article.id, title: article.title }, - }); - return itemList; + const checkArticleExistsInBook = (articleId: number, items: IScrap[]) => { + return items.some((item) => item.article.id === articleId); }; useEffect(() => { + if (selectedBookIndex === -1) return; + const selectedBook = books.find((book) => book.id === selectedBookIndex); + if (!selectedBook || checkArticleExistsInBook(article.id, selectedBook.scraps)) { + setSelectedBookIndex(-1); + setSelectedBookUnavailable(true); + setFilteredScraps([]); + return; + } + + setSelectedBookUnavailable(false); + setFilteredScraps(selectedBook.scraps); + setFilteredScraps(selectedBook ? selectedBook.scraps : []); }, [selectedBookIndex]); @@ -58,7 +76,7 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo const handleScrapBtnClick = () => { if (selectedBookIndex === -1) return; - const scraps = scrapList.map((v: IEditScrap, i: number) => ({ ...v, order: i + 1 })); + const scraps = scrapList.map((v, i) => ({ ...v, order: i + 1 })); createScrap({ book_id: selectedBookIndex, article_id: article.id, scraps }); handleModalClose(); @@ -73,10 +91,17 @@ export default function ScrapModal({ books, handleModalClose, article }: ScrapMo selectedId={selectedBookIndex} handleItemSelect={(id) => setSelectedBookIndex(id)} /> + {isSelectedBookUnavailable && ( + 선택하신 책에 이미 동일한 글이 존재합니다. + )} {filteredScraps.length !== 0 && ( - + )} diff --git a/frontend/components/viewer/ScrapModal/styled.ts b/frontend/components/viewer/ScrapModal/styled.ts index ac45a476..121ccf05 100644 --- a/frontend/components/viewer/ScrapModal/styled.ts +++ b/frontend/components/viewer/ScrapModal/styled.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { TextLarge } from '@styles/common'; +import { TextLarge, TextMedium } from '@styles/common'; export const ScrapModalWrapper = styled.div` margin-top: 32px; @@ -16,3 +16,7 @@ export const ArticleWrapper = styled.div` height: 300px; overflow: auto; `; + +export const WarningLabel = styled(TextMedium)` + color: var(--red-color); +`; diff --git a/frontend/components/viewer/TOC/index.tsx b/frontend/components/viewer/TOC/index.tsx index f9197bd8..242c9e3c 100644 --- a/frontend/components/viewer/TOC/index.tsx +++ b/frontend/components/viewer/TOC/index.tsx @@ -68,7 +68,7 @@ export default function TOC({ articleId, book, handleSideBarOnClick }: TocProps) - + Written by {user.nickname} diff --git a/frontend/components/viewer/TOC/styled.ts b/frontend/components/viewer/TOC/styled.ts index 9580c0ce..bea16deb 100644 --- a/frontend/components/viewer/TOC/styled.ts +++ b/frontend/components/viewer/TOC/styled.ts @@ -24,6 +24,12 @@ export const TocWrapper = styled(Flex)` flex-direction: column; justify-content: space-between; // animation: ${slide} 1s ease-in-out; + + @media ${(props) => props.theme.mobile} { + position: absolute; + z-index: 5; + width: 100%; + } `; export const TocSideBar = styled.div` @@ -74,10 +80,13 @@ export const TocArticle = styled(Link)` } `; -export const TocProfile = styled(Flex)` +export const TocProfile = styled(Link)` + display: flex; justify-content: end; align-items: end; padding: 20px; + text-decoration: none; + color: inherit; `; export const TocProfileText = styled(Flex)` diff --git a/frontend/components/viewer/ViewerHead/index.tsx b/frontend/components/viewer/ViewerHead/index.tsx new file mode 100644 index 00000000..052c5093 --- /dev/null +++ b/frontend/components/viewer/ViewerHead/index.tsx @@ -0,0 +1,21 @@ +import Head from 'next/head'; + +interface ViewerHeadProps { + articleTitle: string; + articleContent: string; +} + +export default function ViewerHead({ articleTitle, articleContent }: ViewerHeadProps) { + return ( + + {articleTitle} + + + + + + + + + ); +} diff --git a/frontend/hooks/useFetch.ts b/frontend/hooks/useFetch.ts index 8e3ce5cd..4711d1aa 100644 --- a/frontend/hooks/useFetch.ts +++ b/frontend/hooks/useFetch.ts @@ -5,10 +5,12 @@ import { toastError } from '@utils/toast'; const useFetch = (api: (...args: any[]) => Promise) => { const [data, setData] = useState(); + const [isLoading, setIsLoading] = useState(true); const execute = useCallback(async (...args: any[]) => { try { setData(await api(...args)); + setIsLoading(false); } catch (error: any) { const { message } = error.response.data; @@ -16,7 +18,7 @@ const useFetch = (api: (...args: any[]) => Promise) => { } }, []); - return { data, execute }; + return { data, isLoading, execute }; }; export default useFetch; diff --git a/frontend/hooks/useInput.ts b/frontend/hooks/useInput.ts index 87da5f94..2d639483 100644 --- a/frontend/hooks/useInput.ts +++ b/frontend/hooks/useInput.ts @@ -7,7 +7,7 @@ const useInput = (initialValue = '') => { setValue(event.target.value); }; - return { value, onChange }; + return { value, setValue, onChange }; }; export default useInput; diff --git a/frontend/interfaces/index.ts b/frontend/interfaces/index.ts index 994b98e9..2cd751bd 100644 --- a/frontend/interfaces/index.ts +++ b/frontend/interfaces/index.ts @@ -5,4 +5,4 @@ import { IArticleBook, IBookScraps } from './combined.interface'; import { IScrap } from './scrap.interface'; import { IUser } from './user.interface'; -export type { IArticle, IBook, IBookmark, IScrap, IUser, IBookScraps, IArticleBook }; +export type { IArticle, IBook, IBookmark, IScrap, IUser, IArticleBook, IBookScraps }; diff --git a/frontend/interfaces/scrap.interface.ts b/frontend/interfaces/scrap.interface.ts index 2975de36..27a3e0c5 100644 --- a/frontend/interfaces/scrap.interface.ts +++ b/frontend/interfaces/scrap.interface.ts @@ -1,13 +1,7 @@ -import { IArticle } from './article.interface'; - export interface IScrap { id: number; order: number; - article: IArticle; -} -export interface IEditScrap { - id: number; - order: number; + is_original: boolean; article: { id: number; title: string; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8eb40a38..58dad363 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@codemirror/lang-markdown": "^6.0.5", "@codemirror/language": "^6.3.1", + "@codemirror/language-data": "^6.1.0", "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.6.0", "@lezer/highlight": "^1.1.2", @@ -24,15 +25,20 @@ "eslint-config-next": "13.0.3", "immutability-helper": "^3.1.1", "next": "13.0.3", + "next-sitemap": "^3.1.32", "react": "18.2.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", + "react-dnd-touch-backend": "^16.0.1", "react-dom": "18.2.0", "react-toastify": "^9.1.1", "recoil": "^0.7.6", + "rehype-parse": "^8.0.4", + "rehype-remark": "^9.1.2", "rehype-stringify": "^9.0.3", "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", + "remark-stringify": "^10.0.2", "styled-components": "^5.3.6", "styled-reset": "^4.4.2", "typescript": "4.8.4", @@ -352,6 +358,15 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz", + "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, "node_modules/@codemirror/lang-css": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.1.tgz", @@ -378,6 +393,15 @@ "@lezer/html": "^1.1.0" } }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", + "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, "node_modules/@codemirror/lang-javascript": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.1.tgz", @@ -392,6 +416,15 @@ "@lezer/javascript": "^1.0.0" } }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, "node_modules/@codemirror/lang-markdown": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.0.5.tgz", @@ -405,6 +438,71 @@ "@lezer/markdown": "^1.0.0" } }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz", + "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.0.tgz", + "integrity": "sha512-a/JhyPYn5qz5T8WtAfZCuAZcfClgNVb7UZzdLr76bWUeG7Usd3Un5o8UQOkZ/5Xw+EM5YGHHG+T6ickMYkDcRQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.0.0", + "@lezer/python": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", + "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.3.3.tgz", + "integrity": "sha512-VNsHju8500fkiDyDU8jZyGQ8M0iXU0SmfeCoCeAYkACcEFlX63BOT8311pICXyw43VYRbS23w54RgSEQmixGjQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.1.tgz", + "integrity": "sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.1.tgz", + "integrity": "sha512-0tvycUTElajCcRKgsszhKjWX+uuOogdu5+enpfqYA+j0gnP8ek7LRxujh2/XMPRdXt/hwOML4slJLE7r2eX3yQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.3.1.tgz", @@ -418,6 +516,36 @@ "style-mod": "^4.0.0" } }, + "node_modules/@codemirror/language-data": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.1.0.tgz", + "integrity": "sha512-g9V23fuLRI9AEbpM6bDy1oquqgpFlIDHTihUhL21NPmxp+x67ZJbsKk+V71W7/Bj8SCqEO1PtqQA/tDGgt1nfw==", + "dependencies": { + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.1.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.1.tgz", + "integrity": "sha512-icXmCs4Mhst2F8mE0TNpmG6l7YTj1uxam3AbZaFaabINH5oWAdg2CfR/PVi+d/rqxJ+TuTnvkKK5GILHrNThtw==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, "node_modules/@codemirror/lint": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz", @@ -453,6 +581,11 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@corex/deepmerge": { + "version": "4.0.29", + "resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.29.tgz", + "integrity": "sha512-q/yVUnqckA8Do+EvAfpy7RLdumnBy9ZsducMUtZTvpdbJC7azEf1hGtnYYxm0QfphYxjwggv6XtH64prvS1W+A==" + }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", @@ -586,6 +719,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.1.tgz", "integrity": "sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw==" }, + "node_modules/@lezer/cpp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.0.0.tgz", + "integrity": "sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/css": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.1.tgz", @@ -613,6 +755,15 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/java": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.0.tgz", + "integrity": "sha512-z2EA0JHq2WoiKfQy5uOOd4t17PJtq8guh58gPkSzOnNcQ7DNbkrU+Axak+jL8+Noinwyz2tRNOseQFj+Tg+P0A==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/javascript": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.1.1.tgz", @@ -622,6 +773,15 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/lr": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz", @@ -639,6 +799,42 @@ "@lezer/highlight": "^1.0.0" } }, + "node_modules/@lezer/php": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.0.tgz", + "integrity": "sha512-kFQu/mk/vmjpA+fjQU87d9eimqKJ9PFCa8CZCPFWGEwNnm7Ahpw32N+HYEU/YAQ0XcfmOAnW/YJCEa8WpUOMMw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.1.tgz", + "integrity": "sha512-ArUGh9kvdaOVu6IkSaYUS9WFQeMAFVWKRuZo6vexnxoeCLnxf0Y9DCFEAMMa7W9SQBGYE55OarSpPqSkdOXSCA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz", + "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.0.tgz", + "integrity": "sha512-73iI9UK8iqSvWtLlOEl/g+50ivwQn8Ge6foHVN66AXUS1RccFnAoc7BYU8b3c8/rP6dfCOGqAGaWLxBzhj60MA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@next/env": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.3.tgz", @@ -969,6 +1165,11 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" }, + "node_modules/@types/extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.1.tgz", + "integrity": "sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw==" + }, "node_modules/@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", @@ -1015,6 +1216,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -3147,6 +3353,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hast-util-embedded": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-2.0.0.tgz", + "integrity": "sha512-vEr54rDu2CheBM4nLkWbW8Rycf8HhkA/KsrDnlyKnvBTyhyO+vAG6twHnfUbiRGo56YeUBNCI4HFfHg3Wu+tig==", + "dependencies": { + "hast-util-is-element": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.0.tgz", + "integrity": "sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz", + "integrity": "sha512-4Qf++8o5v14us4Muv3HRj+Er6wTNGA/N9uCaZMty4JWvyFKLdhULrv4KE1b65AthsSO9TXSZnjuxS8ecIyhb0w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-2.0.0.tgz", + "integrity": "sha512-S58hCexyKdD31vMsErvgLfflW6vYWo/ixRLPJTtkOvLld24vyI8vmYmkgLA5LG3la2ME7nm7dLGdm48gfLRBfw==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-is-element": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-is-element": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz", @@ -3160,6 +3420,33 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-parse-selector": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz", + "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-2.0.1.tgz", + "integrity": "sha512-Lw+gVihgE0Ye1TsToZqui0puQnHbZ0dFQe0c/Z2QJWGYRIc72DpH3UHLV8zU48sjIPord88MTSeYEbLQMp5A9g==", + "dependencies": { + "hast-util-embedded": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-is-body-ok-link": "^2.0.0", + "hast-util-is-element": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-html": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz", @@ -3181,6 +3468,46 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-mdast": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-8.4.1.tgz", + "integrity": "sha512-tfmBLASuCgyhCzpkTXM5kU8xeuS5jkMZ17BYm2YftGT5wvgc7uHXTZ/X8WfNd6F5NV/IGmrLsuahZ+jXQir4zQ==", + "dependencies": { + "@types/extend": "^3.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "extend": "^3.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-phrasing": "^2.0.0", + "hast-util-to-text": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "rehype-minify-whitespace": "^5.0.0", + "trim-trailing-lines": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.1.tgz", + "integrity": "sha512-7S3mOBxACy8syL45hCn3J7rHqYaXkxRfsX6LXEU5Shz4nt4GxdjtMUtG+T6G/ZLUHd7kslFAf14kAN71bz30xA==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "unist-util-find-after": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-whitespace": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", @@ -3190,6 +3517,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.1.0.tgz", + "integrity": "sha512-uBjaTTLN0MkCZxY/R2fWUOcu7FRtUVzKRO5P/RAfgsu3yFiMB1JWCO4AjeVkgHxAira1f2UecHK5WfS9QurlWA==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -3688,6 +4031,15 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3747,6 +4099,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-phrasing": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.0.tgz", + "integrity": "sha512-S+QYsDRLkGi8U7o5JF1agKa/sdP+CNGXXLqC17pdTVL8FHHgQEiwFGa9yE5aYtUxNiFGYoaDy9V1kC85Sz86Gg==", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-hast": { "version": "12.2.4", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.2.4.tgz", @@ -3767,6 +4132,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-markdown": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.3.0.tgz", + "integrity": "sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", @@ -4355,6 +4738,31 @@ } } }, + "node_modules/next-sitemap": { + "version": "3.1.32", + "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-3.1.32.tgz", + "integrity": "sha512-jkIKpwLXpWWTPfmDO46+6nu4+qpar4CjvUwCR9rYZHWtzE/wFfaCVFKpGtFMl6MFjpu8GjiE6kWFEa7uF3bzzg==", + "funding": [ + { + "url": "https://github.com/iamvishnusankar/next-sitemap.git" + } + ], + "dependencies": { + "@corex/deepmerge": "^4.0.29", + "minimist": "^1.2.6" + }, + "bin": { + "next-sitemap": "bin/next-sitemap.mjs", + "next-sitemap-cjs": "bin/next-sitemap.cjs" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "@next/env": "*", + "next": "*" + } + }, "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -4539,6 +4947,11 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4763,6 +5176,15 @@ "dnd-core": "^16.0.1" } }, + "node_modules/react-dnd-touch-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz", + "integrity": "sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -4851,21 +5273,68 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/rehype-stringify": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", - "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==", + "node_modules/rehype-minify-whitespace": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-5.0.1.tgz", + "integrity": "sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==", "dependencies": { "@types/hast": "^2.0.0", - "hast-util-to-html": "^8.0.0", - "unified": "^10.0.0" + "hast-util-embedded": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "unified": "^10.0.0", + "unist-util-is": "^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/remark-parse": { + "node_modules/rehype-parse": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz", + "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^7.0.0", + "parse5": "^6.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-remark": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-9.1.2.tgz", + "integrity": "sha512-c0fG3/CrJ95zAQ07xqHSkdpZybwdsY7X5dNWvgL2XqLKZuqmG3+vk6kP/4miCnp+R+x/0uKKRSpfXb9aGR8Z5w==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "hast-util-to-mdast": "^8.3.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", + "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-to-html": "^8.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz", "integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==", @@ -4894,6 +5363,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.2.tgz", + "integrity": "sha512-6wV3pvbPvHkbNnWB0wdDvVFHOe1hBRAx1Q/5g/EpH4RppAII6J8Gnwe7VbHuXaoKIF6LAg6ExTel/+kNqSQ7lw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -5456,6 +5939,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/trim-trailing-lines": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", + "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trough": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", @@ -5578,6 +6070,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-find-after": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-4.0.0.tgz", + "integrity": "sha512-gfpsxKQde7atVF30n5Gff2fQhAc4/HTOV4CvkXpTg9wRfQhZWdXitpyXHWB6YcYgnsxLx+4gGHeVjCTAAp9sjw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-generated": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz", @@ -5721,6 +6226,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.0.1.tgz", + "integrity": "sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.3.tgz", @@ -5752,6 +6270,15 @@ "node": ">=10.13.0" } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/webpack": { "version": "5.75.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", @@ -5887,6 +6414,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } }, "dependencies": { @@ -6113,6 +6649,15 @@ "@lezer/common": "^1.0.0" } }, + "@codemirror/lang-cpp": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz", + "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, "@codemirror/lang-css": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.1.tgz", @@ -6139,6 +6684,15 @@ "@lezer/html": "^1.1.0" } }, + "@codemirror/lang-java": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", + "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, "@codemirror/lang-javascript": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.1.tgz", @@ -6153,6 +6707,15 @@ "@lezer/javascript": "^1.0.0" } }, + "@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, "@codemirror/lang-markdown": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.0.5.tgz", @@ -6166,6 +6729,71 @@ "@lezer/markdown": "^1.0.0" } }, + "@codemirror/lang-php": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz", + "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==", + "requires": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "@codemirror/lang-python": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.0.tgz", + "integrity": "sha512-a/JhyPYn5qz5T8WtAfZCuAZcfClgNVb7UZzdLr76bWUeG7Usd3Un5o8UQOkZ/5Xw+EM5YGHHG+T6ickMYkDcRQ==", + "requires": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.0.0", + "@lezer/python": "^1.0.0" + } + }, + "@codemirror/lang-rust": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", + "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "@codemirror/lang-sql": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.3.3.tgz", + "integrity": "sha512-VNsHju8500fkiDyDU8jZyGQ8M0iXU0SmfeCoCeAYkACcEFlX63BOT8311pICXyw43VYRbS23w54RgSEQmixGjQ==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@codemirror/lang-wast": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.1.tgz", + "integrity": "sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@codemirror/lang-xml": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.1.tgz", + "integrity": "sha512-0tvycUTElajCcRKgsszhKjWX+uuOogdu5+enpfqYA+j0gnP8ek7LRxujh2/XMPRdXt/hwOML4slJLE7r2eX3yQ==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, "@codemirror/language": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.3.1.tgz", @@ -6179,6 +6807,36 @@ "style-mod": "^4.0.0" } }, + "@codemirror/language-data": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.1.0.tgz", + "integrity": "sha512-g9V23fuLRI9AEbpM6bDy1oquqgpFlIDHTihUhL21NPmxp+x67ZJbsKk+V71W7/Bj8SCqEO1PtqQA/tDGgt1nfw==", + "requires": { + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.1.0" + } + }, + "@codemirror/legacy-modes": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.1.tgz", + "integrity": "sha512-icXmCs4Mhst2F8mE0TNpmG6l7YTj1uxam3AbZaFaabINH5oWAdg2CfR/PVi+d/rqxJ+TuTnvkKK5GILHrNThtw==", + "requires": { + "@codemirror/language": "^6.0.0" + } + }, "@codemirror/lint": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz", @@ -6214,6 +6872,11 @@ "w3c-keyname": "^2.2.4" } }, + "@corex/deepmerge": { + "version": "4.0.29", + "resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.29.tgz", + "integrity": "sha512-q/yVUnqckA8Do+EvAfpy7RLdumnBy9ZsducMUtZTvpdbJC7azEf1hGtnYYxm0QfphYxjwggv6XtH64prvS1W+A==" + }, "@emotion/is-prop-valid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", @@ -6322,6 +6985,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.1.tgz", "integrity": "sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw==" }, + "@lezer/cpp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.0.0.tgz", + "integrity": "sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/css": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.1.tgz", @@ -6349,6 +7021,15 @@ "@lezer/lr": "^1.0.0" } }, + "@lezer/java": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.0.tgz", + "integrity": "sha512-z2EA0JHq2WoiKfQy5uOOd4t17PJtq8guh58gPkSzOnNcQ7DNbkrU+Axak+jL8+Noinwyz2tRNOseQFj+Tg+P0A==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/javascript": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.1.1.tgz", @@ -6358,6 +7039,15 @@ "@lezer/lr": "^1.0.0" } }, + "@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/lr": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz", @@ -6375,6 +7065,42 @@ "@lezer/highlight": "^1.0.0" } }, + "@lezer/php": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.0.tgz", + "integrity": "sha512-kFQu/mk/vmjpA+fjQU87d9eimqKJ9PFCa8CZCPFWGEwNnm7Ahpw32N+HYEU/YAQ0XcfmOAnW/YJCEa8WpUOMMw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/python": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.1.tgz", + "integrity": "sha512-ArUGh9kvdaOVu6IkSaYUS9WFQeMAFVWKRuZo6vexnxoeCLnxf0Y9DCFEAMMa7W9SQBGYE55OarSpPqSkdOXSCA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/rust": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz", + "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/xml": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.0.tgz", + "integrity": "sha512-73iI9UK8iqSvWtLlOEl/g+50ivwQn8Ge6foHVN66AXUS1RccFnAoc7BYU8b3c8/rP6dfCOGqAGaWLxBzhj60MA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@next/env": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.3.tgz", @@ -6573,6 +7299,11 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" }, + "@types/extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/extend/-/extend-3.0.1.tgz", + "integrity": "sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw==" + }, "@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", @@ -6619,6 +7350,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" }, + "@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" + }, "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -8180,6 +8916,44 @@ "has-symbols": "^1.0.2" } }, + "hast-util-embedded": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-2.0.0.tgz", + "integrity": "sha512-vEr54rDu2CheBM4nLkWbW8Rycf8HhkA/KsrDnlyKnvBTyhyO+vAG6twHnfUbiRGo56YeUBNCI4HFfHg3Wu+tig==", + "requires": { + "hast-util-is-element": "^2.0.0" + } + }, + "hast-util-from-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.0.tgz", + "integrity": "sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==", + "requires": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + } + }, + "hast-util-has-property": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz", + "integrity": "sha512-4Qf++8o5v14us4Muv3HRj+Er6wTNGA/N9uCaZMty4JWvyFKLdhULrv4KE1b65AthsSO9TXSZnjuxS8ecIyhb0w==" + }, + "hast-util-is-body-ok-link": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-2.0.0.tgz", + "integrity": "sha512-S58hCexyKdD31vMsErvgLfflW6vYWo/ixRLPJTtkOvLld24vyI8vmYmkgLA5LG3la2ME7nm7dLGdm48gfLRBfw==", + "requires": { + "@types/hast": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-is-element": "^2.0.0" + } + }, "hast-util-is-element": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz", @@ -8189,6 +8963,25 @@ "@types/unist": "^2.0.0" } }, + "hast-util-parse-selector": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz", + "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==", + "requires": { + "@types/hast": "^2.0.0" + } + }, + "hast-util-phrasing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-2.0.1.tgz", + "integrity": "sha512-Lw+gVihgE0Ye1TsToZqui0puQnHbZ0dFQe0c/Z2QJWGYRIc72DpH3UHLV8zU48sjIPord88MTSeYEbLQMp5A9g==", + "requires": { + "hast-util-embedded": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-is-body-ok-link": "^2.0.0", + "hast-util-is-element": "^2.0.0" + } + }, "hast-util-to-html": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz", @@ -8206,11 +8999,55 @@ "unist-util-is": "^5.0.0" } }, + "hast-util-to-mdast": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-8.4.1.tgz", + "integrity": "sha512-tfmBLASuCgyhCzpkTXM5kU8xeuS5jkMZ17BYm2YftGT5wvgc7uHXTZ/X8WfNd6F5NV/IGmrLsuahZ+jXQir4zQ==", + "requires": { + "@types/extend": "^3.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "extend": "^3.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-phrasing": "^2.0.0", + "hast-util-to-text": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "rehype-minify-whitespace": "^5.0.0", + "trim-trailing-lines": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "hast-util-to-text": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.1.tgz", + "integrity": "sha512-7S3mOBxACy8syL45hCn3J7rHqYaXkxRfsX6LXEU5Shz4nt4GxdjtMUtG+T6G/ZLUHd7kslFAf14kAN71bz30xA==", + "requires": { + "@types/hast": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "unist-util-find-after": "^4.0.0" + } + }, "hast-util-whitespace": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==" }, + "hastscript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.1.0.tgz", + "integrity": "sha512-uBjaTTLN0MkCZxY/R2fWUOcu7FRtUVzKRO5P/RAfgsu3yFiMB1JWCO4AjeVkgHxAira1f2UecHK5WfS9QurlWA==", + "requires": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + } + }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -8546,6 +9383,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8591,6 +9433,15 @@ "uvu": "^0.5.0" } }, + "mdast-util-phrasing": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.0.tgz", + "integrity": "sha512-S+QYsDRLkGi8U7o5JF1agKa/sdP+CNGXXLqC17pdTVL8FHHgQEiwFGa9yE5aYtUxNiFGYoaDy9V1kC85Sz86Gg==", + "requires": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + } + }, "mdast-util-to-hast": { "version": "12.2.4", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.2.4.tgz", @@ -8607,6 +9458,20 @@ "unist-util-visit": "^4.0.0" } }, + "mdast-util-to-markdown": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.3.0.tgz", + "integrity": "sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==", + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + } + }, "mdast-util-to-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz", @@ -8928,6 +9793,15 @@ "use-sync-external-store": "1.2.0" } }, + "next-sitemap": { + "version": "3.1.32", + "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-3.1.32.tgz", + "integrity": "sha512-jkIKpwLXpWWTPfmDO46+6nu4+qpar4CjvUwCR9rYZHWtzE/wFfaCVFKpGtFMl6MFjpu8GjiE6kWFEa7uF3bzzg==", + "requires": { + "@corex/deepmerge": "^4.0.29", + "minimist": "^1.2.6" + } + }, "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -9055,6 +9929,11 @@ "callsites": "^3.0.0" } }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9192,6 +10071,15 @@ "dnd-core": "^16.0.1" } }, + "react-dnd-touch-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz", + "integrity": "sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==", + "requires": { + "@react-dnd/invariant": "^4.0.1", + "dnd-core": "^16.0.1" + } + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -9250,6 +10138,41 @@ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" }, + "rehype-minify-whitespace": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-5.0.1.tgz", + "integrity": "sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==", + "requires": { + "@types/hast": "^2.0.0", + "hast-util-embedded": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "unified": "^10.0.0", + "unist-util-is": "^5.0.0" + } + }, + "rehype-parse": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz", + "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==", + "requires": { + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^7.0.0", + "parse5": "^6.0.0", + "unified": "^10.0.0" + } + }, + "rehype-remark": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-9.1.2.tgz", + "integrity": "sha512-c0fG3/CrJ95zAQ07xqHSkdpZybwdsY7X5dNWvgL2XqLKZuqmG3+vk6kP/4miCnp+R+x/0uKKRSpfXb9aGR8Z5w==", + "requires": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "hast-util-to-mdast": "^8.3.0", + "unified": "^10.0.0" + } + }, "rehype-stringify": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", @@ -9281,6 +10204,16 @@ "unified": "^10.0.0" } }, + "remark-stringify": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.2.tgz", + "integrity": "sha512-6wV3pvbPvHkbNnWB0wdDvVFHOe1hBRAx1Q/5g/EpH4RppAII6J8Gnwe7VbHuXaoKIF6LAg6ExTel/+kNqSQ7lw==", + "requires": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.0.0", + "unified": "^10.0.0" + } + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -9635,6 +10568,11 @@ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" }, + "trim-trailing-lines": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", + "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==" + }, "trough": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", @@ -9722,6 +10660,15 @@ "@types/unist": "^2.0.0" } }, + "unist-util-find-after": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-4.0.0.tgz", + "integrity": "sha512-gfpsxKQde7atVF30n5Gff2fQhAc4/HTOV4CvkXpTg9wRfQhZWdXitpyXHWB6YcYgnsxLx+4gGHeVjCTAAp9sjw==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + } + }, "unist-util-generated": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz", @@ -9813,6 +10760,15 @@ "vfile-message": "^3.0.0" } }, + "vfile-location": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.0.1.tgz", + "integrity": "sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==", + "requires": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + } + }, "vfile-message": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.3.tgz", @@ -9837,6 +10793,11 @@ "graceful-fs": "^4.1.2" } }, + "web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==" + }, "webpack": { "version": "5.75.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", @@ -9932,6 +10893,11 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==" } } } diff --git a/frontend/package.json b/frontend/package.json index 3e1addc6..e3450499 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@codemirror/lang-markdown": "^6.0.5", "@codemirror/language": "^6.3.1", + "@codemirror/language-data": "^6.1.0", "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.6.0", "@lezer/highlight": "^1.1.2", @@ -25,15 +26,20 @@ "eslint-config-next": "13.0.3", "immutability-helper": "^3.1.1", "next": "13.0.3", + "next-sitemap": "^3.1.32", "react": "18.2.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", + "react-dnd-touch-backend": "^16.0.1", "react-dom": "18.2.0", "react-toastify": "^9.1.1", "recoil": "^0.7.6", + "rehype-parse": "^8.0.4", + "rehype-remark": "^9.1.2", "rehype-stringify": "^9.0.3", "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", + "remark-stringify": "^10.0.2", "styled-components": "^5.3.6", "styled-reset": "^4.4.2", "typescript": "4.8.4", diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 06bb6780..2abfb6fc 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -4,18 +4,24 @@ import 'react-toastify/dist/ReactToastify.css'; import { ToastContainer } from 'react-toastify'; import { RecoilRoot } from 'recoil'; +import { ThemeProvider } from 'styled-components'; -import CheckSignInByToken from '@components/CheckSignInByToken'; +import CheckSignInStatus from '@components/auth/CheckSignInStatus'; import GlobalStyle from '@styles/GlobalStyle'; +import responsive from '@styles/responsive'; + +import '@styles/font.css'; export default function App({ Component, pageProps }: AppProps) { return ( - + - - - + + + + + ); } diff --git a/frontend/pages/_document.tsx b/frontend/pages/_document.tsx index 78973364..1c9f6eef 100644 --- a/frontend/pages/_document.tsx +++ b/frontend/pages/_document.tsx @@ -43,7 +43,7 @@ export default class MyDocument extends Document { diff --git a/frontend/pages/booktest.tsx b/frontend/pages/booktest.tsx new file mode 100644 index 00000000..1f7a26b6 --- /dev/null +++ b/frontend/pages/booktest.tsx @@ -0,0 +1,26 @@ +import { useEffect } from 'react'; + +import { getOrderedBookListApi } from '@apis/bookApi'; +import Book from '@components/common/Book'; +import useFetch from '@hooks/useFetch'; + +export default function Booktest() { + const { + data: popularBookList, + isLoading: isPopularBookListLoading, + execute: getPopularBookList, + } = useFetch(getOrderedBookListApi); + + useEffect(() => { + getPopularBookList('bookmark'); + }, []); + return ( +
+ {popularBookList && ( + <> + + + )} +
+ ); +} diff --git a/frontend/pages/editor.tsx b/frontend/pages/editor.tsx index de8f66ce..b82246e8 100644 --- a/frontend/pages/editor.tsx +++ b/frontend/pages/editor.tsx @@ -1,36 +1,69 @@ +import { useRouter } from 'next/router'; + import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; +import { getArticleApi } from '@apis/articleApi'; import { getUserKnottedBooksApi } from '@apis/bookApi'; import signInStatusState from '@atoms/signInStatus'; import Modal from '@components/common/Modal'; +import EditHead from '@components/edit/EditHead'; import Editor from '@components/edit/Editor'; +import ModifyModal from '@components/edit/ModifyModal'; import PublishModal from '@components/edit/PublishModal'; import useFetch from '@hooks/useFetch'; +import { IArticle } from '@interfaces'; +import { toastError } from '@utils/toast'; export default function EditorPage() { const [isModalShown, setModalShown] = useState(false); + const [originalArticle, setOriginalArticle] = useState(undefined); const handleModalOpen = () => setModalShown(true); const handleModalClose = () => setModalShown(false); const { data: books, execute: getUserKnottedBooks } = useFetch(getUserKnottedBooksApi); + const { data: article, execute: getArticle } = useFetch(getArticleApi); const user = useRecoilValue(signInStatusState); + const router = useRouter(); + useEffect(() => { getUserKnottedBooks(user.nickname); - }, []); + }, [user.nickname]); + + useEffect(() => { + if (router.query.id) getArticle(router.query.id); + }, [router.query]); + + useEffect(() => { + if (!article) return; + + if (article.book.user.nickname !== user.nickname) { + toastError('수정 권한이 없습니다.'); + router.push('/'); + return; + } + setOriginalArticle(article); + }, [article]); return ( <> - - {isModalShown && ( - - - - )} + + + + {isModalShown && + ((originalArticle && ( + + + + )) || ( + + + + ))} ); } diff --git a/frontend/pages/github.tsx b/frontend/pages/github.tsx index 23e22725..48d6ad75 100644 --- a/frontend/pages/github.tsx +++ b/frontend/pages/github.tsx @@ -6,29 +6,43 @@ import { useSetRecoilState } from 'recoil'; import { githubSignInApi } from '@apis/authApi'; import signInStatusState from '@atoms/signInStatus'; +import Spinner from '@components/common/Spinner'; import useFetch from '@hooks/useFetch'; +import { FlexColumnCenter, FullPageWrapper } from '@styles/layout'; +import { toastError } from '@utils/toast'; export default function Github() { const router = useRouter(); - const { data: user, execute: githubSignIn } = useFetch(githubSignInApi); + const setSignInStatus = useSetRecoilState(signInStatusState); + const { data: user, execute: githubSignIn } = useFetch(githubSignInApi); useEffect(() => { - if (router.query.code) { - githubSignIn({ - code: router.query.code, - }); + if (router.query.error) { + toastError('GitHub 로그인에 실패했습니다.'); + + router.push('/'); } + }, [router.query.error]); + + useEffect(() => { + if (router.query.code) githubSignIn({ code: router.query.code }); }, [router.query.code]); useEffect(() => { if (!user) return; - setSignInStatus({ - ...user, - }); + setSignInStatus({ ...user }); + router.push('/'); }, [user]); - return
Github 로그인 시동 중입니다.....
; + return ( + + + +
GitHub 로그인 중 입니다.
+
+
+ ); } diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 17ea70cb..efc4d888 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,15 +1,53 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { getOrderedBookListApi } from '@apis/bookApi'; import Footer from '@components/common/Footer'; import GNB from '@components/common/GNB'; +import HomeHead from '@components/home/HomeHead'; import Slider from '@components/home/Slider'; import useFetch from '@hooks/useFetch'; import { PageInnerLarge, PageWrapper } from '@styles/layout'; export default function Home() { - const { data: newestBookList, execute: getNewestBookList } = useFetch(getOrderedBookListApi); - const { data: popularBookList, execute: getPopularBookList } = useFetch(getOrderedBookListApi); + const { + data: newestBookList, + isLoading: isNewBookListLoading, + execute: getNewestBookList, + } = useFetch(getOrderedBookListApi); + + const [numberPerPage, setNumberPerPage] = useState(0); + + const resizingHandler = () => { + switch (true) { + case window.innerWidth > 1300: + setNumberPerPage(4); + break; + case window.innerWidth > 1000: + setNumberPerPage(3); + break; + case window.innerWidth > 700: + setNumberPerPage(2); + break; + default: + setNumberPerPage(1); + break; + } + }; + + useEffect(() => { + resizingHandler(); + window.addEventListener('resize', resizingHandler); + + return () => { + window.removeEventListener('resize', resizingHandler); + }; + }, []); + + const { + data: popularBookList, + isLoading: isPopularBookListLoading, + execute: getPopularBookList, + } = useFetch(getOrderedBookListApi); useEffect(() => { getNewestBookList('newest'); @@ -18,12 +56,27 @@ export default function Home() { return ( <> + - {newestBookList && } - {popularBookList && } -