diff --git a/.env b/.env index 6ce0305..1409b98 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ NEXTAUTH_SECRET=Pn9CJbdUk6C9J8+lY6SlmFHkw4NItMpoHJ6ylIwEqrk= -NEXT_PUBLIC_API_URL=https://localhost:7142 \ No newline at end of file +NEXT_PUBLIC_API_URL=https://mangahub.azurewebsites.net \ No newline at end of file diff --git a/src/app/(main)/admin/types/columns.tsx b/src/app/(main)/admin/types/columns.tsx index fbf2b79..49c6e4b 100644 --- a/src/app/(main)/admin/types/columns.tsx +++ b/src/app/(main)/admin/types/columns.tsx @@ -19,18 +19,6 @@ export const columns: ColumnDef[] = [ accessorKey: 'login', header: 'Login', }, - { - accessorKey: 'firstName', - header: 'First name', - }, - { - accessorKey: 'lastName', - header: 'Last name', - }, - { - accessorKey: 'phoneNumber', - header: 'Phone number', - }, { accessorKey: 'showConfidentialInformation', header: 'Show Confidential Information', diff --git a/src/app/(main)/chapter/edit/page.tsx b/src/app/(main)/chapter/edit/page.tsx index 8333281..74cbe3c 100644 --- a/src/app/(main)/chapter/edit/page.tsx +++ b/src/app/(main)/chapter/edit/page.tsx @@ -1,14 +1,123 @@ -import { useRouter } from 'next/router'; +'use client'; + +import { yupResolver } from '@hookform/resolvers/yup'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import * as yup from 'yup'; + +import { FormValues } from './types'; + +import { Input } from '@/shared/components/FormComponents'; +import { Button } from '@/shared/components/ui/button'; +import { useToast } from '@/shared/components/ui/use-toast'; +import useAxiosAuth from '@/shared/hooks/useAxiosAuth'; +import Chapter from '@/shared/models/chapter'; + +const validationSchema: yup.ObjectSchema = yup.object({ + title: yup + .string() + .required('No title provided.') + .min(10, 'Title too short.') + .max(124, 'Title too long.'), + chapterNumber: yup.number().required('No chapter number provided.'), + createdOn: yup.string().required('No release date provided.'), +}); function Page() { + const [chapter, setChapter] = useState(null); + const router = useRouter(); - const { mangaId, chapterId } = router.query; + const queryParams = useSearchParams(); + + const axiosAuth = useAxiosAuth(); + const { toast } = useToast(); + const { + register, + handleSubmit, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(validationSchema), + }); + + const mangaId = queryParams.get('mangaId'); + const chapterId = queryParams.get('chapterId'); + + const fetchChapter = async () => { + const res = await axiosAuth.get(`Chapters`, { + params: { chapterId }, + }); + + setChapter(res.data); + setValue('title', res.data.title); + setValue('chapterNumber', res.data.chapterNumber); + setValue( + 'createdOn', + new Date(res.data.createdOn).toISOString().slice(0, 10), + ); + }; + + useEffect(() => { + fetchChapter(); + }, []); + + const onSubmit: SubmitHandler = async (data) => { + try { + await axiosAuth.post('Chapters', { + ...chapter, + ...data, + chapterId, + mangaId, + }); + + toast({ + title: 'Success', + description: `Chapter "${data.title}" was successfully created!`, + }); + + router.push(`/manga/${mangaId}`); + } catch (error) { + toast({ + title: 'Error occurred!', + variant: 'destructive', + }); + } + }; + + if (!chapter) { + return
Loading...
; + } return ( -
-

Page

-

Param1: {mangaId}

-

Param2: {chapterId}

+
+

Edit chapter

+
+ + + + +
); } diff --git a/src/app/(main)/chapter/edit/types.ts b/src/app/(main)/chapter/edit/types.ts new file mode 100644 index 0000000..2bc54ff --- /dev/null +++ b/src/app/(main)/chapter/edit/types.ts @@ -0,0 +1,11 @@ +export type FormValues = { + title: string; + chapterNumber: number; + createdOn: string; +}; + +export type CreateChapterDto = { + title: string; + chapterNumber: number; + createdOn: Date; +}; diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 77c4689..f18c691 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -26,7 +26,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
-
{children}
+
{children}
diff --git a/src/app/(main)/manga/[mangaId]/[chapterId]/MangaPDF.tsx b/src/app/(main)/manga/[mangaId]/[chapterId]/MangaPDF.tsx index bd4cd38..82a9753 100644 --- a/src/app/(main)/manga/[mangaId]/[chapterId]/MangaPDF.tsx +++ b/src/app/(main)/manga/[mangaId]/[chapterId]/MangaPDF.tsx @@ -1,40 +1,40 @@ -// 'use client'; - -// import { useState } from 'react'; -// import { Document, Page, pdfjs } from 'react-pdf'; - -// import bytesToFile from '@/shared/utils/bytesToImage'; - -// import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; -// import 'react-pdf/dist/esm/Page/TextLayer.css'; - -// pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`; - -// type MangaPDFProps = { -// chapter: string; -// }; - -// function MangaPDF({ chapter }: MangaPDFProps) { -// const [numPages, setNumPages] = useState(); - -// function onDocumentLoadSuccess({ numPages }: { numPages: number }): void { -// setNumPages(numPages); -// } - -// return ( -// -// {Array.from(new Array(numPages), (_, index) => ( -// -// ))} -// -// ); -// } - -// export default MangaPDF; +'use client'; + +import { useState } from 'react'; +import { Document, Page, pdfjs } from 'react-pdf'; + +import bytesToFile from '@/shared/utils/bytesToFile'; + +import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; +import 'react-pdf/dist/esm/Page/TextLayer.css'; + +pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`; + +type MangaPDFProps = { + chapter: string; +}; + +function MangaPDF({ chapter }: MangaPDFProps) { + const [numPages, setNumPages] = useState(); + + function onDocumentLoadSuccess({ numPages }: { numPages: number }): void { + setNumPages(numPages); + } + + return ( + + {Array.from(new Array(numPages), (_, index) => ( + + ))} + + ); +} + +export default MangaPDF; diff --git a/src/app/(main)/manga/[mangaId]/[chapterId]/page.tsx b/src/app/(main)/manga/[mangaId]/[chapterId]/page.tsx index fb346ab..21f2daa 100644 --- a/src/app/(main)/manga/[mangaId]/[chapterId]/page.tsx +++ b/src/app/(main)/manga/[mangaId]/[chapterId]/page.tsx @@ -2,6 +2,8 @@ import { useEffect, useState } from 'react'; +import MangaPDF from './MangaPDF'; + import useAxiosAuth from '@/shared/hooks/useAxiosAuth'; import Chapter from '@/shared/models/chapter'; @@ -40,7 +42,7 @@ function ChapterPage({ params: { chapterId } }: PageProps) {

Chapter info

Chapter title {chapter?.title}

Scans available {chapter?.scans ? 'yes' : 'no'}

- {/* {chapter?.scans && } */} + {chapter?.scans && }
); } diff --git a/src/app/(main)/manga/[mangaId]/components/ChapterCard.tsx b/src/app/(main)/manga/[mangaId]/components/ChapterCard.tsx index fe79d7e..2ac5f99 100644 --- a/src/app/(main)/manga/[mangaId]/components/ChapterCard.tsx +++ b/src/app/(main)/manga/[mangaId]/components/ChapterCard.tsx @@ -1,5 +1,6 @@ -import { LucideUpload } from 'lucide-react'; +import { LucideUpload, Edit, X } from 'lucide-react'; import { useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; import { useRef } from 'react'; import { ChapterDTO } from '../types'; @@ -7,20 +8,56 @@ import { ChapterDTO } from '../types'; import { Button } from '@/shared/components/ui/button'; import { Card } from '@/shared/components/ui/card'; import { useToast } from '@/shared/components/ui/use-toast'; +import { ROUTE } from '@/shared/constants/routes'; import useAxiosAuth from '@/shared/hooks/useAxiosAuth'; type ChapterListProps = { chapter: ChapterDTO; index: number; mangaId: string; + refetchData: () => void; }; -function ChapterCard({ chapter, index, mangaId }: ChapterListProps) { +function ChapterCard({ + chapter, + index, + mangaId, + refetchData, +}: ChapterListProps) { const fileInputRef = useRef(null); const axiosAuth = useAxiosAuth(); const { toast } = useToast(); const router = useRouter(); + const { data: session } = useSession(); + + const handleEdit = async () => { + router.push( + `${ROUTE.EDIT_CHAPTER}?chapterId=${chapter.chapterId}&mangaId=${mangaId}`, + ); + }; + + const handleDelete = async () => { + try { + await axiosAuth.delete('/Chapters', { + params: { chapterId: chapter.chapterId }, + }); + + toast({ + title: 'Success', + description: 'Chapter has been deleted', + }); + + refetchData(); + } catch (error) { + toast({ + title: 'Error', + variant: 'destructive', + description: 'Something went wrong.', + }); + } + }; + const handleImageChange = async ( event: React.ChangeEvent, ) => { @@ -55,34 +92,44 @@ function ChapterCard({ chapter, index, mangaId }: ChapterListProps) { return (
{ - router.push(`/manga/${mangaId}/${chapter.chapterId}`); + router.push(`${ROUTE.MANGA}/${mangaId}/${chapter.chapterId}`); }} >

{index + 1}: {chapter.title}

- - + {session?.user?.accessToken ? ( + <> + + + + + + ) : null}
); } diff --git a/src/app/(main)/manga/[mangaId]/components/Chapters.tsx b/src/app/(main)/manga/[mangaId]/components/Chapters.tsx index 8b949bc..bbb65ec 100644 --- a/src/app/(main)/manga/[mangaId]/components/Chapters.tsx +++ b/src/app/(main)/manga/[mangaId]/components/Chapters.tsx @@ -1,5 +1,6 @@ import { BookPlusIcon } from 'lucide-react'; import { useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; import ChapterCard from './ChapterCard'; import { ChapterDTO } from '../types'; @@ -11,26 +12,35 @@ type ChaptersProps = { className?: string; mangaId: string; chapters: ChapterDTO[]; + refetchData: () => void; }; -function Chapters({ chapters, mangaId, className = '' }: ChaptersProps) { +function Chapters({ + chapters, + mangaId, + className = '', + refetchData, +}: ChaptersProps) { const router = useRouter(); + const { data: session } = useSession(); return (

Chapters

- + {session?.user?.accessToken ? ( + + ) : null}
{chapters?.length !== 0 ? ( @@ -40,6 +50,7 @@ function Chapters({ chapters, mangaId, className = '' }: ChaptersProps) { index={index} chapter={chapter} mangaId={mangaId} + refetchData={refetchData} /> )) ) : ( diff --git a/src/app/(main)/manga/[mangaId]/edit/page.tsx b/src/app/(main)/manga/[mangaId]/edit/page.tsx new file mode 100644 index 0000000..4de9517 --- /dev/null +++ b/src/app/(main)/manga/[mangaId]/edit/page.tsx @@ -0,0 +1,125 @@ +'use client'; + +import { yupResolver } from '@hookform/resolvers/yup'; +import { useRouter } from 'next/navigation'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import * as yup from 'yup'; + +import { FormValues } from './types'; + +import { Input, Select, Textarea } from '@/shared/components/FormComponents'; +import { Button } from '@/shared/components/ui/button'; +import { + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/shared/components/ui/select'; +import { useToast } from '@/shared/components/ui/use-toast'; +import { ROUTE } from '@/shared/constants/routes'; +import useAxiosAuth from '@/shared/hooks/useAxiosAuth'; +import { Genre } from '@/shared/models/genre'; +import capitalizedWords from '@/shared/utils/capitalizedWords'; + +const validationSchema: yup.ObjectSchema = yup.object({ + title: yup + .string() + .required('No title provided.') + .min(10, 'Title too short.'), + genre: yup.string().required('No genre provided.'), + description: yup + .string() + .required('No description provided.') + .min(15, 'Description must be at least 15 characters long.'), + releasedOn: yup.date().required('No release date provided.'), +}); + +function AddMangaPage() { + const { toast } = useToast(); + const axiosAuth = useAxiosAuth(); + const router = useRouter(); + + const { + register, + handleSubmit, + formState: { errors }, + setValue, + getValues, + } = useForm({ + resolver: yupResolver(validationSchema), + }); + + const onSubmit: SubmitHandler = async (data) => { + try { + await axiosAuth.post('Mangas', { + ...data, + genres: {}, + coverImage: '', + genre: Genre[data.genre], + }); + + toast({ + title: 'Success', + description: `Manga "${data.title}" was successfully created!`, + }); + + router.push(ROUTE.MANGA); + } catch (error) { + toast({ + title: 'Error occurred!', + variant: 'destructive', + }); + } + }; + + return ( +
+

Add new manga

+
+ + +