diff --git a/next.config.mjs b/next.config.mjs index 297c9d7..4678774 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,8 +1,4 @@ /** @type {import('next').NextConfig} */ -const nextConfig = { - experimental: { - missingSuspenseWithCSRBailout: false, - }, -}; +const nextConfig = {}; export default nextConfig; diff --git a/src/app/(auth)/login/container/LoginPage.tsx b/src/app/(auth)/login/container/LoginPage.tsx deleted file mode 100644 index 2d453bf..0000000 --- a/src/app/(auth)/login/container/LoginPage.tsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client'; -import * as React from 'react'; - -import LoginForm from '@/app/(auth)/login/container/LoginForm'; -import withAuth from '@/components/hoc/withAuth'; -import PrimaryLink from '@/components/links/PrimaryLink'; -import NextImage from '@/components/NextImage'; -import Typography from '@/components/Typography'; - -export default withAuth(LoginContainer, ['authed']); -function LoginContainer() { - return ( -
-
-
- - Sign In - - - - Belum punya akun?{' '} - Register - -
-
- -
-
-
- ); -} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 2b4bd2a..f7d9ab5 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,12 +1,39 @@ import { Metadata } from 'next'; import * as React from 'react'; -import LoginContainer from '@/app/(auth)/login/container/LoginPage'; +import LoginForm from '@/app/(auth)/login/container/LoginForm'; +import PrimaryLink from '@/components/links/PrimaryLink'; +import NextImage from '@/components/NextImage'; +import Typography from '@/components/Typography'; export const metadata: Metadata = { title: 'Login', }; export default function LoginPage() { - return ; + return ( +
+
+
+ + Sign In + + + + Belum punya akun?{' '} + Register + +
+
+ +
+
+
+ ); } diff --git a/src/app/(auth)/register/container/RegisterPage.tsx b/src/app/(auth)/register/container/RegisterPage.tsx deleted file mode 100644 index f258c33..0000000 --- a/src/app/(auth)/register/container/RegisterPage.tsx +++ /dev/null @@ -1,36 +0,0 @@ -'use client'; -import * as React from 'react'; - -import RegisterForm from '@/app/(auth)/register/container/RegisterForm'; -import withAuth from '@/components/hoc/withAuth'; -import PrimaryLink from '@/components/links/PrimaryLink'; -import NextImage from '@/components/NextImage'; -import Typography from '@/components/Typography'; - -export default withAuth(RegisterContainer, ['authed']); -function RegisterContainer() { - return ( -
-
-
- -
-
- - Sign Up - - - - Sudah punya akun? Login - -
-
-
- ); -} diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index ad3fdfe..e404bab 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -1,11 +1,38 @@ import { Metadata } from 'next'; import * as React from 'react'; -import RegisterContainer from '@/app/(auth)/register/container/RegisterPage'; +import RegisterForm from '@/app/(auth)/register/container/RegisterForm'; +import PrimaryLink from '@/components/links/PrimaryLink'; +import NextImage from '@/components/NextImage'; +import Typography from '@/components/Typography'; export const metadata: Metadata = { title: 'Register', }; + export default function RegisterPage() { - return ; + return ( +
+
+
+ +
+
+ + Sign Up + + + + Sudah punya akun? Login + +
+
+
+ ); } diff --git a/src/app/board/components/StatusBoard.tsx b/src/app/board/components/StatusBoard.tsx new file mode 100644 index 0000000..c63da8c --- /dev/null +++ b/src/app/board/components/StatusBoard.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { FaPlus } from 'react-icons/fa'; + +import TicketBoard from '@/app/board/components/TicketBoard'; +import IconButton from '@/components/buttons/IconButton'; +import Typography from '@/components/Typography'; +import clsxm from '@/lib/clsxm'; +import { taskType } from '@/types/entities/task'; + +type StatusBoardProps = { + title: string; + data: taskType[] | null; +}; + +export default function StatusBoard({ title, data }: StatusBoardProps) { + const ticketCount = data?.length; + return ( +
+
+
+ + {title} + + + {ticketCount} + +
+ +
+
+ {data?.map((task) => )} +
+
+ ); +} diff --git a/src/app/board/components/TicketBoard.tsx b/src/app/board/components/TicketBoard.tsx new file mode 100644 index 0000000..61ca024 --- /dev/null +++ b/src/app/board/components/TicketBoard.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { FaCalendar, FaEdit } from 'react-icons/fa'; + +import IconButton from '@/components/buttons/IconButton'; +import Chips from '@/components/Chips'; +import UnderlineLink from '@/components/links/UnderlineLink'; +import Typography from '@/components/Typography'; +import { randomColor, showFormattedDate } from '@/lib/helper'; +import { taskType } from '@/types/entities/task'; + +export default function TicketBoard({ data }: { data: taskType | null }) { + const tags = data?.tags; + return ( +
+
+
+ {tags?.map((tag) => ( + + {tag} + + ))} +
+ +
+
+ + + {data?.title} + + + + {data?.description} + +
+
+ + + {showFormattedDate(data?.dueDate ?? new Date())} + +
+
+ ); +} diff --git a/src/app/board/container/BoardPage.tsx b/src/app/board/container/BoardPage.tsx new file mode 100644 index 0000000..3ddbb53 --- /dev/null +++ b/src/app/board/container/BoardPage.tsx @@ -0,0 +1,60 @@ +'use client'; +import * as React from 'react'; +import { FaGithub } from 'react-icons/fa'; + +import StatusBoard from '@/app/board/components/StatusBoard'; +import { useGetTickets } from '@/app/board/hooks/query'; +import withAuth from '@/components/hoc/withAuth'; +import PrimaryLink from '@/components/links/PrimaryLink'; +import Loading from '@/components/Loading'; +import Typography from '@/components/Typography'; + +export default withAuth(BoardContainer, ['user']); +function BoardContainer() { + const { boardData, isLoading } = useGetTickets(); + + if (isLoading) { + return ; + } + + const unarchivedTickets = + boardData && boardData.data + ? boardData.data.tasks.filter((ticket) => !ticket.deletedAt) + : []; + + const [backlogTickets, readyTickets, inProgressTickets, doneTickets] = [ + unarchivedTickets.filter((ticket) => ticket.status === 'backlog'), + unarchivedTickets.filter((ticket) => ticket.status === 'ready'), + unarchivedTickets.filter((ticket) => ticket.status === 'in progress'), + unarchivedTickets.filter((ticket) => ticket.status === 'done'), + ]; + + return ( +
+
+ + Kanban Board + + + A board to keep track of projects and tasks. Built with Next.js, + TypeScript, and Tailwind CSS by{' '} + + {'ainunns'} + + +
+
+ + + + +
+
+ ); +} diff --git a/src/app/board/hooks/query.ts b/src/app/board/hooks/query.ts new file mode 100644 index 0000000..0d78572 --- /dev/null +++ b/src/app/board/hooks/query.ts @@ -0,0 +1,14 @@ +'use client'; +import { useQuery } from '@tanstack/react-query'; + +import { ApiResponse } from '@/types/api'; +import { tasksType } from '@/types/entities/task'; + +export const useGetTickets = () => { + const { data: boardData, isPending: isLoading } = useQuery< + ApiResponse + >({ + queryKey: ['/task'], + }); + return { boardData, isLoading }; +}; diff --git a/src/app/board/page.tsx b/src/app/board/page.tsx new file mode 100644 index 0000000..de6e6d5 --- /dev/null +++ b/src/app/board/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next'; +import * as React from 'react'; + +import BoardContainer from '@/app/board/container/BoardPage'; + +export const metadata: Metadata = { + title: 'Home', +}; +export default function BoardPage() { + return ; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 8d60319..a3fe862 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,17 +1,5 @@ -import * as React from 'react'; +import { redirect } from 'next/navigation'; -import Chips from '@/components/Chips'; -import ArrowLink from '@/components/links/ArrowLink'; -import Typography from '@/components/Typography'; - -export default function Home() { - return ( -
- Go to the homepage - Chips - - Kanban board - -
- ); +export default function HomePage() { + redirect('/board'); } diff --git a/src/components/hoc/withAuth.tsx b/src/components/hoc/withAuth.tsx index d9b47a8..7374e24 100644 --- a/src/components/hoc/withAuth.tsx +++ b/src/components/hoc/withAuth.tsx @@ -1,6 +1,6 @@ 'use client'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useRouter } from 'next/navigation'; import * as React from 'react'; import Loading from '@/components/Loading'; @@ -11,6 +11,8 @@ import { ApiResponseUser } from '@/types/api'; import { PermissionListArray } from '@/types/entities/permission-list'; import { UserType } from '@/types/entities/user'; +import { DANGER_TOAST, showToast } from '../Toast'; + type WithAuthProps = { user: UserType; }; @@ -21,8 +23,6 @@ export default function withAuth( ) { function ComponentWithAuth(props: Omit) { const router = useRouter(); - const pathname = usePathname(); - const redirect = useSearchParams().get('redirect'); const { user, isAuthed, isLoading, login, logout, stopLoading } = useAuthStore(); @@ -35,64 +35,48 @@ export default function withAuth( return; } - const loadUser = async () => { - try { - const newUser = await api.get>('/user'); - if (!newUser) throw new Error('User tidak ditemukan'); - login({ ...newUser.data.user, token }); - } catch { - logout(); - } finally { - stopLoading(); - } - }; + if (isAuthed) { + stopLoading(); + return; + } - if (!isAuthed) { - loadUser(); + try { + const res = await api.get>('/user'); + login({ ...res.data.user, token }); + } catch { + logout(); + } finally { + stopLoading(); } - }, [isAuthed, login, logout, stopLoading]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isAuthed]); React.useEffect(() => { - checkAuth(); - - window.addEventListener('focus', checkAuth); - - return () => { - window.removeEventListener('focus', checkAuth); - }; - }, [checkAuth]); + if ( + isLoading || + permissions.includes('all') || + (permissions.includes('authed') && isAuthed) + ) { + return; + } - React.useEffect(() => { - const Redirect = async () => { - if (isAuthed) { - if (permissions.includes('authed')) { - if (redirect) { - router.replace(redirect as string); - } else if (permissions.includes('user')) { - router.replace(`/board?redirect=${pathname}`); - } - } - } else { - if ( - !permissions.includes('authed') && - !permissions.includes('user') - ) { - router.replace(`/login?redirect=${pathname}`); - } - } - }; - if (!isLoading) { - Redirect(); + if (!isAuthed || (user && !permissions.some((p) => p === user.type))) { + showToast('Anda tidak memiliki akses ke halaman ini', DANGER_TOAST); + router.replace('/login'); } - }, [isAuthed, isLoading, pathname, redirect, router]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isAuthed, isLoading]); - if ( - (isLoading || !isAuthed) && - !permissions.includes('authed') && - !permissions.includes('user') - ) - return ; + React.useEffect(() => { + checkAuth(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + if (isLoading) return ; + else if (!isLoading && !isAuthed) { + router.replace('/login'); + return; + } return ; } diff --git a/src/lib/cookies.ts b/src/lib/cookies.ts index 11390e0..52c25f5 100644 --- a/src/lib/cookies.ts +++ b/src/lib/cookies.ts @@ -13,3 +13,11 @@ export const setToken = (token: string) => { export const removeToken = () => { cookies.remove('@kanban/token'); }; + +export const setRedirect = (redirect: string) => { + cookies.set('@kanban/redirect', redirect); +}; + +export const getRedirect = () => { + return cookies.get('@kanban/redirect'); +}; diff --git a/src/lib/helper.ts b/src/lib/helper.ts new file mode 100644 index 0000000..de984eb --- /dev/null +++ b/src/lib/helper.ts @@ -0,0 +1,15 @@ +export const showFormattedDate = (date: Date) => { + return new Date(date).toLocaleDateString('id-ID', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); +}; + +type colorType = 'primary' | 'success' | 'warning' | 'danger'; + +export const randomColor = () => { + const colors: colorType[] = ['primary', 'success', 'warning', 'danger']; + return colors[Math.floor(Math.random() * colors.length)]; +}; diff --git a/src/types/entities/task.ts b/src/types/entities/task.ts new file mode 100644 index 0000000..1ba846b --- /dev/null +++ b/src/types/entities/task.ts @@ -0,0 +1,30 @@ +export type checklistType = { + _id: string; + isDone: boolean; + checklistItem: string; +}; + +export type attachmentType = { + _id: string; + displayText: string; + link: string; +}; + +export type taskType = { + _id: string; + title: string; + description: string; + tags: string[]; + dueDate: Date; + checklists: checklistType[]; + status: 'backlog' | 'ready' | 'in progress' | 'done'; + attachments: attachmentType[]; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date; + __v: number; +}; + +export type tasksType = { + tasks: taskType[]; +}; diff --git a/tsconfig.json b/tsconfig.json index 7b28589..baa12d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, @@ -18,7 +19,8 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "~/*": ["./public/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],