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.
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+ Are you sure?
+
+
+
+
+ Confirm by typing:{' '}
+
+ YES
+
+
+ setConfirmText(event.target.value)}
+ />
+
+
+
+
+
+ Cancel
+
+
+ Yes, delete it
+
+
+
+
+
+ >
+ );
+};
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}
+
+
+
+ : }
+ >
+ {propsData ? 'Update Post' : 'Publish Post'}
+
+
+
+
+ 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 (
- }
- size="sm"
- onClick={handleClick}
- >
+ } onClick={handleClick}>
{title}
);
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) {