diff --git a/app/[locale]/(auth)/login/LoginForm/index.tsx b/app/[locale]/(auth)/login/LoginForm/index.tsx index 6cb7f0a..18775f9 100644 --- a/app/[locale]/(auth)/login/LoginForm/index.tsx +++ b/app/[locale]/(auth)/login/LoginForm/index.tsx @@ -1,9 +1,16 @@ 'use client'; -import { Input, Button, FormLabel, FormControl } from '@/components/chakra'; +import { + Input, + Button, + FormLabel, + FormControl, + Stack, + Flex, +} from '@/components/chakra'; import { useAuth, useEffect, useState, useRouter } from '@/hooks'; -import { signIn } from 'next-auth/react'; import { NextLink } from '@/components/client'; +import { signIn } from 'next-auth/react'; export default function LoginForm() { const router = useRouter(); @@ -29,7 +36,7 @@ export default function LoginForm() { }, [isAuthenticated, router]); return ( - <> + 0 && !validEmail} @@ -54,26 +61,28 @@ export default function LoginForm() { /> - + + - - + + + ); } diff --git a/app/[locale]/(auth)/login/page.tsx b/app/[locale]/(auth)/login/page.tsx index f497d84..1af3809 100644 --- a/app/[locale]/(auth)/login/page.tsx +++ b/app/[locale]/(auth)/login/page.tsx @@ -3,10 +3,11 @@ import LoginForm from './LoginForm'; export default function Login() { return ( - + Login + ); diff --git a/app/[locale]/(auth)/signup/SignUpForm/index.tsx b/app/[locale]/(auth)/signup/SignUpForm/index.tsx index 41bca21..67c5a6e 100644 --- a/app/[locale]/(auth)/signup/SignUpForm/index.tsx +++ b/app/[locale]/(auth)/signup/SignUpForm/index.tsx @@ -7,17 +7,15 @@ import { FormLabel, FormControl, FormErrorMessage, + Stack, + Flex, } from '@/components/chakra'; import { MIN_PASSWORD_LENGTH } from '@/utils/constants'; import { httpClient } from '@/utils/httpClient'; -import { useEffect, useRouter, useState } from '@/hooks'; -import type { Session } from 'next-auth'; +import { useRouter, useState } from '@/hooks'; import { NextLink } from '@/components/client'; -type PropTypes = { - session?: Session | null; -}; -export default function SignUpForm({ session }: PropTypes) { +export default function SignUpForm() { const router = useRouter(); const [isLoading, setLoading] = useState(false); const [agreed, setAgreed] = useState(false); @@ -33,12 +31,6 @@ export default function SignUpForm({ session }: PropTypes) { const validForm = validEmail && isPasswordStrong && isConfirmMatched; const colorScheme = validForm ? 'brand' : 'gray'; - useEffect(() => { - if (session?.user?.id) { - router.push('/protected'); - } - }, [session?.user?.id, router]); - const handleSignUp = async () => { setLoading(true); await httpClient.post('/api/users', credentials); @@ -47,7 +39,7 @@ export default function SignUpForm({ session }: PropTypes) { }; return ( - <> + Name - + + - - + + + ); } diff --git a/app/[locale]/(auth)/signup/page.tsx b/app/[locale]/(auth)/signup/page.tsx index d0ace62..1e7e126 100644 --- a/app/[locale]/(auth)/signup/page.tsx +++ b/app/[locale]/(auth)/signup/page.tsx @@ -1,16 +1,21 @@ import { Heading, Stack } from '@/components/chakra'; import SignUpForm from './SignUpForm'; import { getSession } from '@/utils/auth'; +import { redirect } from 'next/navigation'; export default async function SignUp() { const session = await getSession(); + if (session) { + redirect('/'); + } + return ( - + Sign Up - + ); } diff --git a/app/[locale]/(custom)/layout.tsx b/app/[locale]/(custom)/layout.tsx index 0ab4519..0946d75 100644 --- a/app/[locale]/(custom)/layout.tsx +++ b/app/[locale]/(custom)/layout.tsx @@ -1,5 +1,5 @@ import { LocaleSwitcher, Navbar } from '@/components/client'; -import { Box, Flex } from '@/components/chakra'; +import { Box, Container, Flex } from '@/components/chakra'; import { ProfileMenu } from '@/components/server'; import type { ReactNode } from '@/types'; import type { Locale } from '@/configs/i18n.config'; @@ -14,15 +14,23 @@ export default function CustomLayout({ return ( - + - + {children} - + ); } diff --git a/app/[locale]/(default)/dashboard/pages/[id]/page.tsx b/app/[locale]/(default)/dashboard/pages/[id]/page.tsx index 95d1498..f8f7771 100644 --- a/app/[locale]/(default)/dashboard/pages/[id]/page.tsx +++ b/app/[locale]/(default)/dashboard/pages/[id]/page.tsx @@ -1,7 +1,4 @@ -import { Flex, Heading, Spinner, Stack } from '@/components/chakra'; -import { GoBackButton } from '@/components/client'; import { prisma } from '@/utils/prisma'; -import { Suspense } from 'react'; import { PageForm } from '../components'; @@ -14,19 +11,5 @@ export default async function EditPage({ params }: EditPageProps) { const data = await prisma.page.findUnique({ where: { id } }); - return ( - - - - - - Edit Page - - - - }> - {data && } - - - ); + return ; } diff --git a/app/[locale]/(default)/dashboard/pages/components/PageForm/index.tsx b/app/[locale]/(default)/dashboard/pages/components/PageForm/index.tsx index 82bd385..5b67098 100644 --- a/app/[locale]/(default)/dashboard/pages/components/PageForm/index.tsx +++ b/app/[locale]/(default)/dashboard/pages/components/PageForm/index.tsx @@ -9,19 +9,30 @@ import { Select, Stack, } from '@/components/chakra'; -import { CustomEditable, FormWrapper, TextEditor } from '@/components/client'; +import { + CustomEditable, + FormWrapper, + GoBackButton, + TextEditor, +} from '@/components/client'; import { useEffect, useRouter, useState, useToast } from '@/hooks'; -import { RepeatIcon } from '@/icons'; +import { ArrowUpIcon, RepeatIcon } from '@/icons'; import { createPage, updatePage } from '@/services/pages'; import { Page } from '@/types'; import slugify from 'slugify'; import { DeleteSection } from './DeleteSection'; export type PageFormProps = { - data?: Page; + title?: string; + backPath?: string; + data?: Page | null; }; -export const PageForm = ({ data: propsData }: PageFormProps) => { +export const PageForm = ({ + backPath, + title, + data: propsData, +}: PageFormProps) => { const toast = useToast(); const router = useRouter(); @@ -70,88 +81,99 @@ export const PageForm = ({ data: propsData }: PageFormProps) => { return ( - - - - Title - - setInputData({ ...data, title: event.target.value }) - } - /> - - - - Slug: - - - {data.slug && ( - { - setIsCustomEdited(true); - setInputData({ ...data, slug: newValue }); - }} - /> - )} - - {isCustomEdited && ( - } - onClick={() => { - setIsCustomEdited(false); - setInputData({ - ...data, - slug: slugify(data.title), - }); - }} - /> - )} - - - - - Content - - setInputData({ ...data, content: newValue }) - } - /> - - - - Locale - - - - - - - - {propsData && } - - + + + + Title + + setInputData({ ...data, title: event.target.value }) + } + /> + + + + Slug: + + + {data.slug && ( + { + setIsCustomEdited(true); + setInputData({ ...data, slug: newValue }); + }} + /> + )} + + {isCustomEdited && ( + } + onClick={() => { + setIsCustomEdited(false); + setInputData({ + ...data, + slug: slugify(data.title), + }); + }} + /> + )} + + + + + Content + + setInputData({ ...data, content: newValue }) + } + /> + + + + Locale + + + + {propsData && } + ); }; diff --git a/app/[locale]/(default)/dashboard/pages/new/page.tsx b/app/[locale]/(default)/dashboard/pages/new/page.tsx index 97d06ca..4bd2d1f 100644 --- a/app/[locale]/(default)/dashboard/pages/new/page.tsx +++ b/app/[locale]/(default)/dashboard/pages/new/page.tsx @@ -1,23 +1,5 @@ -import { Flex, Heading, Spinner, Stack } from '@/components/chakra'; -import { GoBackButton } from '@/components/client'; -import { Suspense } from 'react'; - import { PageForm } from '../components'; export default function AddNewPage() { - return ( - - - - - - Add New Page - - - - }> - - - - ); + return ; } diff --git a/app/[locale]/(default)/dashboard/posts/[id]/page.tsx b/app/[locale]/(default)/dashboard/posts/[id]/page.tsx new file mode 100644 index 0000000..99d93e0 --- /dev/null +++ b/app/[locale]/(default)/dashboard/posts/[id]/page.tsx @@ -0,0 +1,15 @@ +import { prisma } from '@/utils/prisma'; + +import { PostForm } from '../components'; + +type EditPostProps = { + params: { id: string }; +}; + +export default async function EditPost({ params }: EditPostProps) { + const { id } = params; + + const data = await prisma.post.findUnique({ where: { id } }); + + return ; +} diff --git a/app/[locale]/(default)/dashboard/posts/components/PostForm/DeleteSection.tsx b/app/[locale]/(default)/dashboard/posts/components/PostForm/DeleteSection.tsx new file mode 100644 index 0000000..9ecdb48 --- /dev/null +++ b/app/[locale]/(default)/dashboard/posts/components/PostForm/DeleteSection.tsx @@ -0,0 +1,166 @@ +import { + AlertDialog, + AlertDialogBody, + AlertDialogCloseButton, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Button, + Card, + CardBody, + CardFooter, + CardHeader, + Divider, + Flex, + FormControl, + FormLabel, + Heading, + Input, + Skeleton, + Stack, + Text, +} from '@/components/chakra'; +import { + useColorModeValue, + useDisclosure, + useRef, + useRouter, + useState, + useToast, +} from '@/hooks'; +import { deletePost } from '@/services/posts'; +import type { Post } from '@/types'; + +export type PostDeleteSectionProps = { + post: Post; +}; + +export const DeleteSection = ({ post }: PostDeleteSectionProps) => { + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + const toast = useToast(); + const cancelRef = useRef(null); + + const { isOpen, onOpen, onClose } = useDisclosure(); + + const [confirmText, setConfirmText] = useState(''); + + const postId = post?.id ?? ''; + + const handleCancel = () => { + setConfirmText(''); + onClose(); + }; + + const handleDelete = async () => { + try { + if (!post) return; + + setIsLoading(true); + + await deletePost(postId); + + toast({ description: 'Delete successfully' }); + + setIsLoading(false); + + onClose(); + + router.refresh(); + router.push('/dashboard/posts'); + } catch (error: any) { + toast({ status: 'error', title: `Error: ${error.message}` }); + } + }; + + const backgroundColor = useColorModeValue('red.50', 'red.900'); + const footerBorder = useColorModeValue('red.100', 'red.600'); + + if (!postId) { + return ; + } + + return ( + <> + + + Delete Post + + + + + + The post will be permanently deleted. This action is irreversible + and can not be undone. + + + + + + + + + + + + + + + + + + Are you sure? + + + + + Confirm by typing:{' '} + + YES + + + setConfirmText(event.target.value)} + /> + + + + + + + + + + + + ); +}; diff --git a/app/[locale]/(default)/dashboard/posts/components/PostForm/index.tsx b/app/[locale]/(default)/dashboard/posts/components/PostForm/index.tsx new file mode 100644 index 0000000..31fdf4b --- /dev/null +++ b/app/[locale]/(default)/dashboard/posts/components/PostForm/index.tsx @@ -0,0 +1,179 @@ +import { + Button, + Flex, + FormControl, + FormLabel, + Heading, + IconButton, + Input, + Select, + Stack, +} from '@/components/chakra'; +import { + CustomEditable, + FormWrapper, + GoBackButton, + TextEditor, +} from '@/components/client'; +import { useEffect, useRouter, useState, useToast } from '@/hooks'; +import { ArrowUpIcon, RepeatIcon } from '@/icons'; +import { createPost, updatePost } from '@/services/posts'; +import { Post } from '@/types'; +import slugify from 'slugify'; +import { DeleteSection } from './DeleteSection'; + +export type PostFormProps = { + title?: string; + backPath?: string; + data?: Post | null; +}; + +export const PostForm = ({ + backPath, + title, + data: propsData, +}: PostFormProps) => { + const toast = useToast(); + const router = useRouter(); + + const [isLoading, setIsLoading] = useState(false); + + const [data, setInputData] = useState({ + title: propsData?.title ?? '', + content: propsData?.content ?? '', + slug: propsData?.slug ?? '', + locale: propsData?.locale ?? 'vi', + }); + + const [isCustomEdited, setIsCustomEdited] = useState(false); + + useEffect(() => { + if (!isCustomEdited) { + setInputData((prev) => ({ + ...prev, + slug: slugify(prev.title), + })); + } + }, [data.title, isCustomEdited]); + + const handleSubmit = async (formData: FormData) => { + setIsLoading(true); + + if (propsData) { + const response = await updatePost(propsData.id, formData); + + if (response) { + toast({ description: 'Cập nhật thành công' }); + router.refresh(); + } + } else { + const response = await createPost(formData); + + if (response) { + toast({ description: 'Published successfully' }); + router.refresh(); + router.push(`/dashboard/posts/${response.id}`); + } + } + + setIsLoading(false); + }; + + return ( + + + + + + + + {title} + + + + + + + + Title + + setInputData({ ...data, title: event.target.value }) + } + /> + + + + Slug: + + + {data.slug && ( + { + setIsCustomEdited(true); + setInputData({ ...data, slug: newValue }); + }} + /> + )} + + {isCustomEdited && ( + } + onClick={() => { + setIsCustomEdited(false); + setInputData({ + ...data, + slug: slugify(data.title), + }); + }} + /> + )} + + + + + Content + + setInputData({ ...data, content: newValue }) + } + /> + + + + Locale + + + + {propsData && } + + + ); +}; diff --git a/app/[locale]/(default)/dashboard/posts/components/PostsTable/index.tsx b/app/[locale]/(default)/dashboard/posts/components/PostsTable/index.tsx new file mode 100644 index 0000000..4cbc9b1 --- /dev/null +++ b/app/[locale]/(default)/dashboard/posts/components/PostsTable/index.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { VirtualTable } from '@/components/client'; +import { Text } from '@/components/chakra'; +import { useRouter } from '@/hooks'; +import type { Post } from '@/types'; + +export type PostTableProps = { + data: Post[]; +}; + +export const PostTable = ({ data }: PostTableProps) => { + const router = useRouter(); + + return data.length > 0 ? ( + router.push(`/dashboard/posts/${item.id}`)} + /> + ) : ( + No posts + ); +}; diff --git a/app/[locale]/(default)/dashboard/posts/components/index.ts b/app/[locale]/(default)/dashboard/posts/components/index.ts new file mode 100644 index 0000000..366e38f --- /dev/null +++ b/app/[locale]/(default)/dashboard/posts/components/index.ts @@ -0,0 +1,4 @@ +'use client'; + +export * from './PostForm'; +export * from './PostsTable'; diff --git a/app/[locale]/(default)/dashboard/posts/new/page.tsx b/app/[locale]/(default)/dashboard/posts/new/page.tsx new file mode 100644 index 0000000..22b53ef --- /dev/null +++ b/app/[locale]/(default)/dashboard/posts/new/page.tsx @@ -0,0 +1,5 @@ +import { PostForm } from '../components'; + +export default function AddNewPost() { + return ; +} diff --git a/app/[locale]/(default)/dashboard/posts/page.tsx b/app/[locale]/(default)/dashboard/posts/page.tsx index 2039e5a..f6ea60f 100644 --- a/app/[locale]/(default)/dashboard/posts/page.tsx +++ b/app/[locale]/(default)/dashboard/posts/page.tsx @@ -1,3 +1,26 @@ -export default function PostsDashboard() { - return 'post managment here'; +import { Flex, Heading, Spacer } from '@/components/chakra'; +import { AddNewButton } from '@/components/client'; +import { prisma } from '@/utils/prisma'; +import { PostTable } from './components'; + +export default async function PostsDashboard() { + const posts = await prisma.post.findMany({ + include: { author: true }, + }); + + return ( + + + + Posts + + + + + + + + + + ); } diff --git a/components/client/AddNewButton/index.tsx b/components/client/AddNewButton/index.tsx index b366de9..6eb87d3 100644 --- a/components/client/AddNewButton/index.tsx +++ b/components/client/AddNewButton/index.tsx @@ -22,12 +22,7 @@ export const AddNewButton = ({ }; return ( - ); diff --git a/components/client/LocaleSwitcher/index.tsx b/components/client/LocaleSwitcher/index.tsx index a50aa2a..3e22218 100644 --- a/components/client/LocaleSwitcher/index.tsx +++ b/components/client/LocaleSwitcher/index.tsx @@ -31,7 +31,7 @@ export function LocaleSwitcher({ locale }: LocaleSwitcherProps) {