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"],