diff --git a/.eslintrc.json b/.eslintrc.json index 21c0b611..49811cbb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -102,6 +102,12 @@ } } ], + "@typescript-eslint/consistent-type-imports": [ + "error", + { + "prefer": "type-imports" + } + ], "dot-notation": "off", "@typescript-eslint/dot-notation": "error" }, @@ -109,7 +115,8 @@ { "files": ["src/stories/**/*.{js,ts,tsx}"], "rules": { - "no-console": "off" + "no-console": "off", + "@typescript-eslint/consistent-type-imports": "off" } } ] diff --git a/src/app/(auth)/AuthProvider.tsx b/src/app/(auth)/AuthProvider.tsx index 71f34aa4..3caabbc1 100644 --- a/src/app/(auth)/AuthProvider.tsx +++ b/src/app/(auth)/AuthProvider.tsx @@ -3,8 +3,8 @@ import { useEffect } from "react"; import { clientSignIn, clientSignOut } from "@/store/features/auth/authSlice"; import { useAppDispatch } from "@/store/hooks"; -import { User, getUserState } from "@/store/features/user/userSlice"; -import { AppError } from "@/types/types"; +import { type User, getUserState } from "@/store/features/user/userSlice"; +import { type AppError } from "@/types/types"; interface AuthProviderProps { user: User | null; diff --git a/src/app/(auth)/authService.ts b/src/app/(auth)/authService.ts index d7a77abe..4a90f119 100644 --- a/src/app/(auth)/authService.ts +++ b/src/app/(auth)/authService.ts @@ -1,9 +1,9 @@ "use server"; import { cookies } from "next/headers"; -import { AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; +import { type AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; import { getAccessToken, getRefreshToken } from "@/utils/getCookie"; -import { POST } from "@/utils/requests"; +import { POST, UNAUTHPOST } from "@/utils/requests"; interface AuthResponse { message: string; @@ -13,12 +13,23 @@ interface ServerSignInResponse extends AuthResponse {} interface ServerSignOutResponse extends AuthResponse {} +interface ServerSignInProps { + email: string; + password: string; +} + +interface ResetPasswordRequestProps { + email?: string; + token?: string; + password?: string; +} + // prettier-ignore // prettier causing issues here with eslint rules -export async function serverSignIn(): Promise< +export async function serverSignIn({ email, password }: ServerSignInProps ): Promise< AsyncActionResponse > { - const userOrError = async () => asyncSignIn(); + const userOrError = async () => asyncSignIn(email, password); return handleAsync(userOrError); } @@ -46,9 +57,44 @@ export async function serverSignOut(): Promise< } +export async function resetPasswordRequestEmail( + email: string, +): Promise> { + const asyncPasswordResetEmail = async () => + UNAUTHPOST( + "api/v1/auth/reset-password/request", + "no-store", + { + email, + }, + ); + + return handleAsync(asyncPasswordResetEmail); +} + +export async function resetPassword({ + password, + token, +}: ResetPasswordRequestProps): Promise> { + const asyncResetPassword = async () => + UNAUTHPOST( + "api/v1/auth/reset-password", + "no-store", + { + password, + token, + }, + ); + + return handleAsync(asyncResetPassword); +} + ///////////////////////////////////////////////////////////////////////////// -async function asyncSignIn(): Promise { +async function asyncSignIn( + email: string, + password: string, +): Promise { try { const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/login`, @@ -59,12 +105,12 @@ async function asyncSignIn(): Promise { "Content-Type": "application/json", }, body: JSON.stringify({ - email: "l.castro@outlook.com", - password: "password", + email, + password, }), credentials: "include", cache: "no-store", - } + }, ); if (!res.ok) { diff --git a/src/app/(auth)/components/AuthBannerContainer.tsx b/src/app/(auth)/components/AuthBannerContainer.tsx index 160ca816..6cbe68d4 100644 --- a/src/app/(auth)/components/AuthBannerContainer.tsx +++ b/src/app/(auth)/components/AuthBannerContainer.tsx @@ -2,7 +2,7 @@ import Banner from "@/components/banner/Banner"; export default function AuthBannerContainer() { return ( -
+
-
-
{children}
+
+
+
+ + {children} +
+
); diff --git a/src/app/(auth)/sign-in/components/EmailCheckContainer.tsx b/src/app/(auth)/sign-in/components/EmailCheckContainer.tsx index 829de67d..de7fb8f2 100644 --- a/src/app/(auth)/sign-in/components/EmailCheckContainer.tsx +++ b/src/app/(auth)/sign-in/components/EmailCheckContainer.tsx @@ -1,38 +1,69 @@ -import Image from "next/image"; +import { type Dispatch, type SetStateAction } from "react"; +import { ContainerState } from "./SignInContainer"; import Button from "@/components/Button"; +import Banner from "@/components/banner/Banner"; +import { useAppDispatch } from "@/store/hooks"; +import useServerAction from "@/hooks/useServerAction"; +import { resetPasswordRequestEmail } from "@/app/(auth)/authService"; +import { onOpenModal } from "@/store/features/modal/modalSlice"; +import Spinner from "@/components/Spinner"; + +type ResendEmailContainerProp = { + email: string; + setContainerState: Dispatch>; +}; + +function EmailCheckContainer({ + email, + setContainerState, +}: ResendEmailContainerProp) { + const dispatch = useAppDispatch(); + + const { + runAction: resetPwdReqEmailAction, + isLoading: resetPwdReqEmailLoading, + setIsLoading: setResetPwdReqEmailLoading, + } = useServerAction(resetPasswordRequestEmail); + + async function handleResendEmail() { + const [, error] = await resetPwdReqEmailAction(email); + + if (error) { + dispatch( + onOpenModal({ type: "error", content: { message: error.message } }), + ); + } + + setResetPwdReqEmailLoading(false); + } + + function renderButtonContent() { + if (resetPwdReqEmailLoading) { + return ; + } + return "Resend Email"; + } + + function handleClick() { + setContainerState(ContainerState.SignIn); + } -function EmailCheckContainer() { return (

- Welcome to Chingu + Reset Link Sent

-
- Light login image -
-
- Light login image +
-

+

Check Your Email Address

@@ -40,7 +71,7 @@ function EmailCheckContainer() { to reset your password. Please open it and click on the link in it to reset your password.

-

+

If you have not received an email shortly, then please check your spam/trash folders or click the button below to request a new reset email. @@ -48,10 +79,14 @@ function EmailCheckContainer() {

+
); diff --git a/src/app/(auth)/sign-in/components/NewPasswordContainer.tsx b/src/app/(auth)/sign-in/components/NewPasswordContainer.tsx deleted file mode 100644 index 95dcae3a..00000000 --- a/src/app/(auth)/sign-in/components/NewPasswordContainer.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useForm, SubmitHandler } from "react-hook-form"; -import * as z from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import TextInput from "@/components/inputs/TextInput"; -import Button from "@/components/Button"; -import { validateTextInput } from "@/helpers/form/validateInput"; - -const validationSchema = z.object({ - password: validateTextInput({ - inputName: "Password", - required: true, - minLen: 8, - maxLen: 20, - }), -}); - -type ValidationSchema = z.infer; - -type NewPasswordContainerProps = { - onClick: () => void; -}; - -function NewPasswordContainer({ onClick }: NewPasswordContainerProps) { - const { - register, - formState: { errors }, - handleSubmit, - } = useForm({ - resolver: zodResolver(validationSchema), - }); - - const onSubmit: SubmitHandler = () => { - onClick(); - }; - - return ( -
-

- Create New Password -

-

- Enter in a new password below to finish resetting your password. -

-
-
-
- -
-
-
- -
-
-
- ); -} - -export default NewPasswordContainer; diff --git a/src/app/(auth)/sign-in/components/ResetCompletedContainer.tsx b/src/app/(auth)/sign-in/components/ResetCompletedContainer.tsx deleted file mode 100644 index a216dfd7..00000000 --- a/src/app/(auth)/sign-in/components/ResetCompletedContainer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import Image from "next/image"; -import Button from "@/components/Button"; - -type ResetCompletedContainerProps = { - onClick: () => void; -}; - -function ResetCompletedContainer({ onClick }: ResetCompletedContainerProps) { - return ( -
-

- Password Reset! -

-
- Reset password confirmed image -
-
- Reset password confirmed image -
-

- Password Reset Successfully -

-

- Your password has been reset, please click the button below to sign in - to access your Chingu account. -

- -
- ); -} - -export default ResetCompletedContainer; diff --git a/src/app/(auth)/sign-in/components/ResetPasswordContainer.tsx b/src/app/(auth)/sign-in/components/ResetPasswordContainer.tsx index 62514d26..07638f11 100644 --- a/src/app/(auth)/sign-in/components/ResetPasswordContainer.tsx +++ b/src/app/(auth)/sign-in/components/ResetPasswordContainer.tsx @@ -1,11 +1,17 @@ -import { useForm, SubmitHandler } from "react-hook-form"; -import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; import Link from "next/link"; +import { type SubmitHandler, useForm } from "react-hook-form"; +import { type Dispatch, type SetStateAction } from "react"; +import { resetPasswordRequestEmail } from "@/app/(auth)/authService"; import Button from "@/components/Button"; import TextInput from "@/components/inputs/TextInput"; +import { onOpenModal } from "@/store/features/modal/modalSlice"; +import { useAppDispatch } from "@/store/hooks"; import { validateTextInput } from "@/helpers/form/validateInput"; import routePaths from "@/utils/routePaths"; +import useServerAction from "@/hooks/useServerAction"; +import Spinner from "@/components/Spinner"; const validationSchema = z.object({ email: validateTextInput({ @@ -19,23 +25,55 @@ type ValidationSchema = z.infer; interface ResetPasswordContainerProps { handleEmailCheck: () => void; + setEmail: Dispatch>; } function ResetPasswordContainer({ handleEmailCheck, + setEmail, }: ResetPasswordContainerProps) { + const dispatch = useAppDispatch(); + + const { + runAction: resetPwdReqEmailAction, + isLoading: resetPwdReqEmailLoading, + setIsLoading: setResetPwdReqEmailLoading, + } = useServerAction(resetPasswordRequestEmail); + const { register, - formState: { errors }, + formState: { errors, isDirty, isValid }, handleSubmit, } = useForm({ + mode: "onTouched", resolver: zodResolver(validationSchema), }); - const onSubmit: SubmitHandler = () => { - handleEmailCheck(); + const onSubmit: SubmitHandler = async (data) => { + const { email } = data; + const [res, error] = await resetPwdReqEmailAction(email); + + if (res) { + handleEmailCheck(); + setEmail(email); + } + + if (error) { + dispatch( + onOpenModal({ type: "error", content: { message: error.message } }), + ); + } + + setResetPwdReqEmailLoading(false); }; + function renderButtonContent() { + if (resetPwdReqEmailLoading) { + return ; + } + return "Send reset link"; + } + return (

@@ -44,27 +82,25 @@ function ResetPasswordContainer({

Enter your email below and we’ll send you a link to reset your password

-
+ Don’t have an account? Sign up for an account now diff --git a/src/app/(auth)/sign-in/components/SignInBlock.tsx b/src/app/(auth)/sign-in/components/SignInBlock.tsx index 62ff4f84..613294a6 100644 --- a/src/app/(auth)/sign-in/components/SignInBlock.tsx +++ b/src/app/(auth)/sign-in/components/SignInBlock.tsx @@ -16,7 +16,7 @@ function SignInBlock({ handleResetPassword }: SignInBlockProps) {

-

Or

+

Or


diff --git a/src/app/(auth)/sign-in/components/SignInContainer.tsx b/src/app/(auth)/sign-in/components/SignInContainer.tsx index afda8bbc..61c4687a 100644 --- a/src/app/(auth)/sign-in/components/SignInContainer.tsx +++ b/src/app/(auth)/sign-in/components/SignInContainer.tsx @@ -3,18 +3,16 @@ import { useState } from "react"; import ResetPasswordContainer from "./ResetPasswordContainer"; import SignInBlock from "./SignInBlock"; -import NewPasswordContainer from "./NewPasswordContainer"; -import ResetCompletedContainer from "./ResetCompletedContainer"; import EmailCheckContainer from "./EmailCheckContainer"; +export enum ContainerState { + SignIn, + ResetPassword, + EmailCheck, +} + function SignInContainer() { - enum ContainerState { - SignIn, - ResetPassword, - EmailCheck, - NewPassword, - ResetCompleted, - } + const [email, setEmail] = useState(""); const [containerState, setContainerState] = useState( ContainerState.SignIn, @@ -28,29 +26,23 @@ function SignInContainer() { setContainerState(ContainerState.EmailCheck); }; - const handleNewPassword = () => { - setContainerState(ContainerState.ResetCompleted); - }; - - const handleResetConfirmed = () => { - setContainerState(ContainerState.SignIn); - }; - return ( <> {containerState === ContainerState.ResetPassword && ( - + + )} + {containerState === ContainerState.EmailCheck && ( + )} - {containerState === ContainerState.EmailCheck && } {containerState === ContainerState.SignIn && ( )} - {containerState === ContainerState.NewPassword && ( - - )} - {containerState === ContainerState.ResetCompleted && ( - - )} ); } diff --git a/src/app/(auth)/sign-in/components/SignInFormContainer.tsx b/src/app/(auth)/sign-in/components/SignInFormContainer.tsx index 7d570592..e0a282f7 100644 --- a/src/app/(auth)/sign-in/components/SignInFormContainer.tsx +++ b/src/app/(auth)/sign-in/components/SignInFormContainer.tsx @@ -1,12 +1,21 @@ -import Link from "next/link"; -import { useForm, SubmitHandler } from "react-hook-form"; import * as z from "zod"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { type SubmitHandler, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { serverSignIn } from "@/app/(auth)/authService"; + import Button from "@/components/Button"; import TextInput from "@/components/inputs/TextInput"; import { validateTextInput } from "@/helpers/form/validateInput"; +import { clientSignIn } from "@/store/features/auth/authSlice"; +import { onOpenModal } from "@/store/features/modal/modalSlice"; +import { useAppDispatch } from "@/store/hooks"; import routePaths from "@/utils/routePaths"; +import useServerAction from "@/hooks/useServerAction"; +import Spinner from "@/components/Spinner"; + const validationSchema = z.object({ email: validateTextInput({ inputName: "Email", @@ -30,29 +39,58 @@ interface SignInFormContainerProps { function SignInFormContainer({ handleResetPassword, }: SignInFormContainerProps) { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const { + runAction: serverSignInAction, + isLoading: serverSignInLoading, + setIsLoading: setServerSignInLoading, + } = useServerAction(serverSignIn); + const { register, - formState: { errors }, + formState: { errors, isDirty, isValid }, handleSubmit, } = useForm({ + mode: "onTouched", resolver: zodResolver(validationSchema), }); - const onSubmit: SubmitHandler = () => {}; + const onSubmit: SubmitHandler = async (data) => { + const { email, password } = data; + const [res, error] = await serverSignInAction({ email, password }); + + if (res) { + dispatch(clientSignIn()); + router.replace(routePaths.dashboardPage()); + } + + if (error) { + dispatch( + onOpenModal({ type: "error", content: { message: error.message } }), + ); + setServerSignInLoading(false); + } + }; + + function renderButtonContent() { + if (serverSignInLoading) { + return ; + } + return "Sign In"; + } return ( - +
-
+
Forgot your password?
-
+
Don’t have an account? Sign up for an account now diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx index 8fef0378..321962cc 100644 --- a/src/app/(auth)/sign-in/page.tsx +++ b/src/app/(auth)/sign-in/page.tsx @@ -1,11 +1,5 @@ import SignInContainer from "./components/SignInContainer"; -import AuthBannerContainer from "@/app/(auth)/components/AuthBannerContainer"; export default function SignInPage() { - return ( -
- - -
- ); + return ; } diff --git a/src/app/(auth)/sign-up/components/ConfirmationMailContainer.tsx b/src/app/(auth)/sign-up/components/ConfirmationMailContainer.tsx index a022eb02..a734b41a 100644 --- a/src/app/(auth)/sign-up/components/ConfirmationMailContainer.tsx +++ b/src/app/(auth)/sign-up/components/ConfirmationMailContainer.tsx @@ -1,34 +1,19 @@ -import Image from "next/image"; import Button from "@/components/Button"; +import Banner from "@/components/banner/Banner"; function ConfirmationMailContainer() { return (

- Welcome to Chingu + Welcome to Chingu!

-
- Light login image -
-
- Light login image +
@@ -48,7 +33,8 @@ function ConfirmationMailContainer() { diff --git a/src/app/(auth)/sign-up/components/SignUpContainer.tsx b/src/app/(auth)/sign-up/components/SignUpContainer.tsx index af13811b..01eabc9e 100644 --- a/src/app/(auth)/sign-up/components/SignUpContainer.tsx +++ b/src/app/(auth)/sign-up/components/SignUpContainer.tsx @@ -20,13 +20,13 @@ function SignUpContainer() { ) : (

- Welcome to Chingu + Create a Chingu Account


-

Or

+

Or


diff --git a/src/app/(auth)/sign-up/components/SignUpFormContainer.tsx b/src/app/(auth)/sign-up/components/SignUpFormContainer.tsx index 4e602d6a..c23a9d89 100644 --- a/src/app/(auth)/sign-up/components/SignUpFormContainer.tsx +++ b/src/app/(auth)/sign-up/components/SignUpFormContainer.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { useForm, SubmitHandler } from "react-hook-form"; +import { useForm, type SubmitHandler } from "react-hook-form"; import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import Button from "@/components/Button"; @@ -34,6 +34,7 @@ function SignUpFormContainer({ formState: { errors }, handleSubmit, } = useForm({ + mode: "onTouched", resolver: zodResolver(validationSchema), }); @@ -42,10 +43,7 @@ function SignUpFormContainer({ }; return ( - +
- Already have an account? Sign in now diff --git a/src/app/(auth)/sign-up/page.tsx b/src/app/(auth)/sign-up/page.tsx index 5c510df2..247bd221 100644 --- a/src/app/(auth)/sign-up/page.tsx +++ b/src/app/(auth)/sign-up/page.tsx @@ -1,11 +1,5 @@ import SignUpContainer from "./components/SignUpContainer"; -import AuthBannerContainer from "@/app/(auth)/components/AuthBannerContainer"; export default function SignInPage() { - return ( -
- - -
- ); + return ; } diff --git a/src/app/(auth)/users/components/NewPasswordContainer.tsx b/src/app/(auth)/users/components/NewPasswordContainer.tsx new file mode 100644 index 00000000..4badcad4 --- /dev/null +++ b/src/app/(auth)/users/components/NewPasswordContainer.tsx @@ -0,0 +1,113 @@ +import { useForm, type SubmitHandler } from "react-hook-form"; +import { useSearchParams } from "next/navigation"; +import * as z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { resetPassword } from "@/app/(auth)/authService"; +import TextInput from "@/components/inputs/TextInput"; +import Button from "@/components/Button"; +import { onOpenModal } from "@/store/features/modal/modalSlice"; +import { useAppDispatch } from "@/store/hooks"; +import { validateTextInput } from "@/helpers/form/validateInput"; +import useServerAction from "@/hooks/useServerAction"; +import Spinner from "@/components/Spinner"; + +const validationSchema = z.object({ + password: validateTextInput({ + inputName: "Password", + required: true, + minLen: 8, + maxLen: 20, + }), +}); + +type ValidationSchema = z.infer; + +type NewPasswordContainerProps = { + onClick: () => void; +}; + +function NewPasswordContainer({ onClick }: NewPasswordContainerProps) { + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + const dispatch = useAppDispatch(); + + const { + register, + formState: { errors, isDirty, isValid }, + handleSubmit, + } = useForm({ + mode: "onTouched", + resolver: zodResolver(validationSchema), + }); + + const { + runAction: resetPasswordAction, + isLoading: resetPasswordLoading, + setIsLoading: setResetPasswordLoading, + } = useServerAction(resetPassword); + + const onSubmit: SubmitHandler = async (data) => { + if (token) { + const [res, error] = await resetPasswordAction({ + password: data.password, + token, + }); + + if (res) { + onClick(); + } + + if (error) { + dispatch( + onOpenModal({ type: "error", content: { message: error.message } }), + ); + } + + setResetPasswordLoading(false); + } + }; + + function renderButtonContent() { + if (resetPasswordLoading) { + return ; + } + return "Update New Password"; + } + + return ( +
+

+ Create New Password +

+

+ Enter in a new password below to finish resetting your password. +

+ +
+
+ +
+
+
+ +
+ +
+ ); +} + +export default NewPasswordContainer; diff --git a/src/app/(auth)/users/components/ResetCompletedContainer.tsx b/src/app/(auth)/users/components/ResetCompletedContainer.tsx new file mode 100644 index 00000000..d22191e5 --- /dev/null +++ b/src/app/(auth)/users/components/ResetCompletedContainer.tsx @@ -0,0 +1,33 @@ +import Link from "next/link"; +import Button from "@/components/Button"; +import routePaths from "@/utils/routePaths"; +import Banner from "@/components/banner/Banner"; + +function ResetCompletedContainer() { + return ( +
+

+ Password Reset! +

+ +

+ Password Reset Successfully +

+

+ Your password has been reset, please click the button below to sign in + to access your Chingu account. +

+ + + +
+ ); +} + +export default ResetCompletedContainer; diff --git a/src/app/(auth)/users/components/ResetPasswordContainer.tsx b/src/app/(auth)/users/components/ResetPasswordContainer.tsx new file mode 100644 index 00000000..3dae7784 --- /dev/null +++ b/src/app/(auth)/users/components/ResetPasswordContainer.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useState } from "react"; +import NewPasswordContainer from "./NewPasswordContainer"; +import ResetCompletedContainer from "./ResetCompletedContainer"; + +enum ContainerState { + NewPassword, + ResetCompleted, +} + +function ResetPasswordContainer() { + const [containerState, setContainerState] = useState( + ContainerState.NewPassword, + ); + + const handleNewPassword = () => { + setContainerState(ContainerState.ResetCompleted); + }; + + return ( + <> + {containerState === ContainerState.NewPassword && ( + + )} + {containerState === ContainerState.ResetCompleted && ( + + )} + + ); +} + +export default ResetPasswordContainer; diff --git a/src/app/(auth)/users/reset-password/page.tsx b/src/app/(auth)/users/reset-password/page.tsx new file mode 100644 index 00000000..d990b35a --- /dev/null +++ b/src/app/(auth)/users/reset-password/page.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import ResetPasswordContainer from "@/app/(auth)/users/components/ResetPasswordContainer"; + +export default function ResetPasswordPage() { + return ; +} diff --git a/src/app/(main)/dashboard/components/Calendar/Calendar.logic.ts b/src/app/(main)/dashboard/components/Calendar/Calendar.logic.ts index 05df86ba..b7e88385 100644 --- a/src/app/(main)/dashboard/components/Calendar/Calendar.logic.ts +++ b/src/app/(main)/dashboard/components/Calendar/Calendar.logic.ts @@ -14,7 +14,7 @@ import { endOfDay, } from "date-fns"; import { useState } from "react"; -import { SprintData } from "@/app/(main)/dashboard/mocks/voyageDashboardData"; +import { type SprintData } from "@/app/(main)/dashboard/mocks/voyageDashboardData"; export const useCalendarLogic = (sprintData?: SprintData) => { const currentDate = new Date(); diff --git a/src/app/(main)/my-voyage/[teamId]/directory/components/DirectoryComponentWrapper.tsx b/src/app/(main)/my-voyage/[teamId]/directory/components/DirectoryComponentWrapper.tsx index fcedd4d6..baa90aa3 100644 --- a/src/app/(main)/my-voyage/[teamId]/directory/components/DirectoryComponentWrapper.tsx +++ b/src/app/(main)/my-voyage/[teamId]/directory/components/DirectoryComponentWrapper.tsx @@ -1,13 +1,13 @@ import { redirect } from "next/navigation"; import DirectoryProvider from "./DirectoryProvider"; import TeamMember from "./TeamMember"; -import { TeamDirectory } from "@/store/features/directory/directorySlice"; +import { type TeamDirectory } from "@/store/features/directory/directorySlice"; import Banner from "@/components/banner/Banner"; import { getAccessToken } from "@/utils/getCookie"; -import { AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; +import { type AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; import { GET } from "@/utils/requests"; import { CacheTag } from "@/utils/cacheTag"; -import { User } from "@/store/features/user/userSlice"; +import { type User } from "@/store/features/user/userSlice"; import { getUser } from "@/utils/getUser"; import { getTimezone } from "@/utils/getTimezone"; import VoyagePageBannerContainer from "@/components/banner/VoyagePageBannerContainer"; @@ -29,7 +29,7 @@ export async function fetchTeamDirectory({ `api/v1/teams/${teamId}`, token, "force-cache", - CacheTag.directory + CacheTag.directory, ); const [res, error] = await handleAsync(fetchTeamDirectoryAsync); @@ -38,7 +38,7 @@ export async function fetchTeamDirectory({ updateDirectoryWithCurrentTime(res); const teamMember = res.voyageTeamMembers; const elementToSort = teamMember.find( - (element) => element.member.discordId === user?.discordId + (element) => element.member.discordId === user?.discordId, ); moveElementToFirst(teamMember, elementToSort); } @@ -128,10 +128,7 @@ export default async function DirectoryComponentWrapper({
{/* data */} {teamDirectory.voyageTeamMembers.map((teamMember) => ( - + ))}
diff --git a/src/app/(main)/my-voyage/[teamId]/directory/components/DirectoryProvider.tsx b/src/app/(main)/my-voyage/[teamId]/directory/components/DirectoryProvider.tsx index a0f64257..46cc6b9e 100644 --- a/src/app/(main)/my-voyage/[teamId]/directory/components/DirectoryProvider.tsx +++ b/src/app/(main)/my-voyage/[teamId]/directory/components/DirectoryProvider.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { - TeamDirectory, + type TeamDirectory, fetchTeamDirectory, } from "@/store/features/directory/directorySlice"; import { useAppDispatch } from "@/store/hooks"; diff --git a/src/app/(main)/my-voyage/[teamId]/directory/components/EditHours.tsx b/src/app/(main)/my-voyage/[teamId]/directory/components/EditHours.tsx index 85c2ba3a..7186cd86 100644 --- a/src/app/(main)/my-voyage/[teamId]/directory/components/EditHours.tsx +++ b/src/app/(main)/my-voyage/[teamId]/directory/components/EditHours.tsx @@ -1,9 +1,9 @@ import * as z from "zod"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { type SubmitHandler, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useParams } from "next/navigation"; import { PencilSquareIcon } from "@heroicons/react/24/outline"; -import { SetStateAction, useEffect } from "react"; +import { type SetStateAction, useEffect } from "react"; import TextInput from "@/components/inputs/TextInput"; import { validateTextInput } from "@/helpers/form/validateInput"; import { useAppDispatch } from "@/store/hooks"; @@ -66,7 +66,7 @@ export default function EditHours({ if (error) { dispatch( - onOpenModal({ type: "error", content: { message: error.message } }) + onOpenModal({ type: "error", content: { message: error.message } }), ); } diff --git a/src/app/(main)/my-voyage/[teamId]/directory/components/TeamMember.tsx b/src/app/(main)/my-voyage/[teamId]/directory/components/TeamMember.tsx index c33a4d49..f4b46bb3 100644 --- a/src/app/(main)/my-voyage/[teamId]/directory/components/TeamMember.tsx +++ b/src/app/(main)/my-voyage/[teamId]/directory/components/TeamMember.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react"; import TeamMemberDataItemWrapper from "./TeamMemberDataItemWrapper"; import EditHours from "./EditHours"; import { cn } from "@/lib/utils"; -import { VoyageTeam } from "@/store/features/directory/directorySlice"; +import { type VoyageTeam } from "@/store/features/directory/directorySlice"; import { useUser } from "@/store/hooks"; interface TeamMemberProps { @@ -40,7 +40,7 @@ export default function TeamMember({ teamMember }: TeamMemberProps) {
@@ -58,10 +58,7 @@ export default function TeamMember({ teamMember }: TeamMemberProps) { > {voyageRole.name} - +
+ {(provided: DraggableProvided) => (
  • ) : ( // Creator's avatar - + )}
  • diff --git a/src/app/(main)/my-voyage/[teamId]/features/components/EditPopover.tsx b/src/app/(main)/my-voyage/[teamId]/features/components/EditPopover.tsx index 24358d95..bbd8109c 100644 --- a/src/app/(main)/my-voyage/[teamId]/features/components/EditPopover.tsx +++ b/src/app/(main)/my-voyage/[teamId]/features/components/EditPopover.tsx @@ -1,5 +1,5 @@ import { PencilSquareIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { Dispatch, SetStateAction } from "react"; +import { type Dispatch, type SetStateAction } from "react"; import Button from "@/components/Button"; import { useAppDispatch } from "@/store/hooks"; import { onOpenModal } from "@/store/features/modal/modalSlice"; @@ -41,7 +41,7 @@ export default function EditPopover({ redirect: null, deleteFunction: deleteFeature, }, - }) + }), ); } diff --git a/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesComponentWrapper.tsx b/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesComponentWrapper.tsx index 28f6269b..2715965a 100644 --- a/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesComponentWrapper.tsx +++ b/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesComponentWrapper.tsx @@ -5,12 +5,12 @@ import { getCurrentVoyageData } from "@/utils/getCurrentVoyageData"; import { getUser } from "@/utils/getUser"; import { getAccessToken } from "@/utils/getCookie"; import { - Features, - FeaturesList, + type Features, + type FeaturesList, } from "@/store/features/features/featuresSlice"; import { GET } from "@/utils/requests"; import { CacheTag } from "@/utils/cacheTag"; -import { AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; +import { type AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; import VoyagePageBannerContainer from "@/components/banner/VoyagePageBannerContainer"; import Banner from "@/components/banner/Banner"; @@ -46,7 +46,7 @@ function transformData(features: Features[]): FeaturesList[] { } = feature; const existingCategory = transformedData.find( - (item) => item.categoryId === category.id + (item) => item.categoryId === category.id, ); if (existingCategory) { @@ -81,7 +81,7 @@ export async function fetchFeatures({ `api/v1/voyages/teams/${teamId}/features`, token, "force-cache", - CacheTag.features + CacheTag.features, ); const [res, error] = await handleAsync(fetchFeaturesAsync); diff --git a/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesContainer.tsx b/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesContainer.tsx index 985a04ce..e7224ed5 100644 --- a/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesContainer.tsx +++ b/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesContainer.tsx @@ -1,11 +1,11 @@ "use client"; import { useEffect, useState } from "react"; -import { DragDropContext, DropResult } from "@hello-pangea/dnd"; +import { DragDropContext, type DropResult } from "@hello-pangea/dnd"; // import { FeaturesList } from "./fixtures/Features"; import List from "./List"; -import { FeaturesList } from "@/store/features/features/featuresSlice"; +import { type FeaturesList } from "@/store/features/features/featuresSlice"; import { saveOrder } from "@/myVoyage/features/featuresService"; import { useAppDispatch } from "@/store/hooks"; import { onOpenModal } from "@/store/features/modal/modalSlice"; @@ -41,10 +41,10 @@ export default function FeaturesContainer({ data }: FeaturesContainerProps) { // source and destination lists const sourceList = newOrderedData.find( - (list) => list.categoryId.toString() === source.droppableId + (list) => list.categoryId.toString() === source.droppableId, ); const destList = newOrderedData.find( - (list) => list.categoryId.toString() === destination.droppableId + (list) => list.categoryId.toString() === destination.droppableId, ); if (!sourceList || !destList) { @@ -74,7 +74,7 @@ export default function FeaturesContainer({ data }: FeaturesContainerProps) { if (error) { setOrderedData(data); dispatch( - onOpenModal({ type: "error", content: { message: error.message } }) + onOpenModal({ type: "error", content: { message: error.message } }), ); } } @@ -110,7 +110,7 @@ export default function FeaturesContainer({ data }: FeaturesContainerProps) { if (error) { setOrderedData(data); dispatch( - onOpenModal({ type: "error", content: { message: error.message } }) + onOpenModal({ type: "error", content: { message: error.message } }), ); } } diff --git a/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesProvider.tsx b/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesProvider.tsx index d83dfd9e..a1b2bd1c 100644 --- a/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesProvider.tsx +++ b/src/app/(main)/my-voyage/[teamId]/features/components/FeaturesProvider.tsx @@ -4,7 +4,7 @@ import { useEffect } from "react"; import { useAppDispatch } from "@/store/hooks"; import { fetchFeatures, - FeaturesList, + type FeaturesList, } from "@/store/features/features/featuresSlice"; export interface FeaturesProviderProps { diff --git a/src/app/(main)/my-voyage/[teamId]/features/components/List.tsx b/src/app/(main)/my-voyage/[teamId]/features/components/List.tsx index e7fa739c..ad57b3af 100644 --- a/src/app/(main)/my-voyage/[teamId]/features/components/List.tsx +++ b/src/app/(main)/my-voyage/[teamId]/features/components/List.tsx @@ -1,14 +1,14 @@ import { Droppable, - DroppableProvided, - DroppableStateSnapshot, + type DroppableProvided, + type DroppableStateSnapshot, } from "@hello-pangea/dnd"; import { motion } from "framer-motion"; import { useEffect, useRef, useState } from "react"; // import { Feature } from "./fixtures/Features"; import AddFeaturesInput from "./AddFeaturesInput"; import ListItem from "./ListItem"; -import { Features } from "@/store/features/features/featuresSlice"; +import { type Features } from "@/store/features/features/featuresSlice"; interface ListProps { id: number; diff --git a/src/app/(main)/my-voyage/[teamId]/features/components/ListItem.tsx b/src/app/(main)/my-voyage/[teamId]/features/components/ListItem.tsx index 7e9bdd3b..40d20871 100644 --- a/src/app/(main)/my-voyage/[teamId]/features/components/ListItem.tsx +++ b/src/app/(main)/my-voyage/[teamId]/features/components/ListItem.tsx @@ -1,11 +1,11 @@ import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { type SubmitHandler, useForm } from "react-hook-form"; import { useEffect, useRef, useState } from "react"; import Card from "./Card"; import TextInput from "@/components/inputs/TextInput"; import { validateTextInput } from "@/helpers/form/validateInput"; -import { Features } from "@/store/features/features/featuresSlice"; +import { type Features } from "@/store/features/features/featuresSlice"; import useServerAction from "@/hooks/useServerAction"; import { editFeature } from "@/myVoyage/features/featuresService"; import { useAppDispatch } from "@/store/hooks"; @@ -60,7 +60,7 @@ export default function ListItem({ feature, index }: ListItemProps) { if (error) { dispatch( - onOpenModal({ type: "error", content: { message: error.message } }) + onOpenModal({ type: "error", content: { message: error.message } }), ); } @@ -104,10 +104,7 @@ export default function ListItem({ feature, index }: ListItemProps) { }, [editMode, setFocus]); return editMode ? ( -
    +
    (); const { id } = useUser(); @@ -35,11 +49,11 @@ function ContributionCard({ }, [member, id]); return ( -
    +

    Contributed By

    - {ownVote ? ( + {ownVote && !isIdeationFinalized ? ( ); } - -export default ContributionCard; diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/components/FinalizeIdeationButton.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/components/FinalizeIdeationButton.tsx index d71df91e..c110f412 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/components/FinalizeIdeationButton.tsx +++ b/src/app/(main)/my-voyage/[teamId]/ideation/components/FinalizeIdeationButton.tsx @@ -16,7 +16,10 @@ export default function FinalizeIdeationButton() { variant="secondary" size="lg" className="w-full" - disabled={ideation.length === 0} + disabled={ + ideation.length === 0 || + !ideation.some((i) => i.projectIdeaVotes.length > 0) + } > Finalize Selection diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/components/FinalizedIdeationCard.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/components/FinalizedIdeationCard.tsx new file mode 100644 index 00000000..7ae5f4e2 --- /dev/null +++ b/src/app/(main)/my-voyage/[teamId]/ideation/components/FinalizedIdeationCard.tsx @@ -0,0 +1,15 @@ +import { CheckBadgeIcon } from "@heroicons/react/24/outline"; + +export default function FinalizedIdeationCard() { + return ( +
    + +

    Finalized Project

    +

    + Congrats on finalizing your project! Get ready to dive in and remember + to check back here if you need a refresher on what your project idea is! +

    +

    Good luck!

    +
    + ); +} diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationComponentWrapper.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationComponentWrapper.tsx index ca24ac49..62e25b8c 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationComponentWrapper.tsx +++ b/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationComponentWrapper.tsx @@ -2,12 +2,15 @@ import { redirect } from "next/navigation"; import IdeationContainer from "./IdeationContainer"; import IdeationProvider from "./IdeationProvider"; import CreateIdeationContainer from "./CreateIdeationContainer"; -import { FetchIdeationsProps } from "@/app/(main)/my-voyage/[teamId]/ideation/ideationService"; -import { IdeationData } from "@/store/features/ideation/ideationSlice"; +import ContributionCard from "./ContributionCard"; +import VoteCard from "./VoteCard"; +import FinalizedIdeationCard from "./FinalizedIdeationCard"; +import { type FetchIdeationsProps } from "@/app/(main)/my-voyage/[teamId]/ideation/ideationService"; +import { type IdeationData } from "@/store/features/ideation/ideationSlice"; import { getAccessToken } from "@/utils/getCookie"; import { GET } from "@/utils/requests"; import Banner from "@/components/banner/Banner"; -import { AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; +import { type AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; import { CacheTag } from "@/utils/cacheTag"; import VoyagePageBannerContainer from "@/components/banner/VoyagePageBannerContainer"; import { getCurrentVoyageData } from "@/utils/getCurrentVoyageData"; @@ -29,7 +32,7 @@ export async function fetchProjectIdeas({ `api/v1/voyages/teams/${teamId}/ideations`, token, "force-cache", - CacheTag.ideation + CacheTag.ideation, ); return await handleAsync(fetchProjectIdeasAsync); @@ -74,6 +77,28 @@ export default async function IdeationComponentWrapper({ } function renderProjects() { + const finalizedIdeation = projectIdeas.find( + (project) => project.isSelected === true, + ); + + if (finalizedIdeation) { + return ( + } + secondChild={ + + } + /> + ); + } + if (projectIdeas.length === 0) { return (
    @@ -105,18 +130,34 @@ export default async function IdeationComponentWrapper({ ); } - return projectIdeas.map((projectIdea) => ( - - )); + return ( + <> + + {projectIdeas.map((projectIdea) => ( + + } + secondChild={ + + } + /> + ))} + + ); } return ( @@ -136,7 +177,6 @@ export default async function IdeationComponentWrapper({ />
    - {renderProjects()}
    diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationContainer.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationContainer.tsx index a39d85de..9d1553ad 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationContainer.tsx +++ b/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationContainer.tsx @@ -1,35 +1,29 @@ -import ContributionCard from "./ContributionCard"; -import VoteCard from "./VoteCard"; // import type { Ideation } from "./fixtures/ideation"; -import { - ProjectIdeaVotes, - VoyageMember, -} from "@/store/features/ideation/ideationSlice"; interface IdeationContainerProps { - projectIdeaId: number; title: string; project_idea: string; vision_statement: string; - users: ProjectIdeaVotes[]; - contributed_by: { - member: VoyageMember; - }; - teamId: number; + isIdeationFinalized: boolean; + firstChild: JSX.Element; + secondChild: JSX.Element; } export default function IdeationContainer({ - projectIdeaId, title, project_idea, vision_statement, - users, - contributed_by, - teamId, + isIdeationFinalized, + firstChild, + secondChild, }: IdeationContainerProps) { return ( -
    - +
    + {firstChild}

    {title} @@ -47,10 +41,7 @@ export default function IdeationContainer({ {vision_statement}

    - + {secondChild}
    ); } diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationForm.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationForm.tsx index 598eafe7..95f3aa1e 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationForm.tsx +++ b/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { type SubmitHandler, useForm } from "react-hook-form"; import { useParams, useRouter } from "next/navigation"; import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationProvider.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationProvider.tsx index 3374cccf..208e61dc 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationProvider.tsx +++ b/src/app/(main)/my-voyage/[teamId]/ideation/components/IdeationProvider.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { - IdeationData, + type IdeationData, fetchIdeations, setProjectIdeasLoadingFalse, } from "@/store/features/ideation/ideationSlice"; diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/components/VoteCard.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/components/VoteCard.tsx index 92588d5d..4fc2dc6f 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/components/VoteCard.tsx +++ b/src/app/(main)/my-voyage/[teamId]/ideation/components/VoteCard.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from "react"; import Button from "@/components/Button"; import { - ProjectIdeaVotes, + type ProjectIdeaVotes, setProjectIdeasLoadingTrue, } from "@/store/features/ideation/ideationSlice"; import { useAppDispatch, useIdeation, useModal, useUser } from "@/store/hooks"; @@ -126,7 +126,7 @@ function VoteCard({ teamId, projectIdeaId, users, className }: VoteCardProps) { }, [id, getVoteUsers]); return ( -
    +

    {users.length}

    {`Vote${ diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/finalize/components/ConfirmationButton.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/finalize/components/ConfirmationButton.tsx index e69de29b..ca3344d0 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/finalize/components/ConfirmationButton.tsx +++ b/src/app/(main)/my-voyage/[teamId]/ideation/finalize/components/ConfirmationButton.tsx @@ -0,0 +1,66 @@ +import { useParams, useRouter } from "next/navigation"; +import { type FinalizedIdeation } from "./FinalizeIdeationList"; +import useServerAction from "@/hooks/useServerAction"; +import Button from "@/components/Button"; +import { finalizeIdeation } from "@/myVoyage/ideation/ideationService"; +import routePaths from "@/utils/routePaths"; +import { useAppDispatch } from "@/store/hooks"; +import { onOpenModal } from "@/store/features/modal/modalSlice"; +import Spinner from "@/components/Spinner"; + +interface ConfirmationButtonProps { + finalizedIdeation: FinalizedIdeation; +} + +export default function ConfirmationButton({ + finalizedIdeation, +}: ConfirmationButtonProps) { + const params = useParams<{ teamId: string }>(); + const teamId = Number(params.teamId); + const router = useRouter(); + const dispatch = useAppDispatch(); + + const { + runAction: finalizeIdeationAction, + isLoading: finalizeIdeationLoading, + setIsLoading: setFinalizeIdeationLoading, + } = useServerAction(finalizeIdeation); + + async function handleClick() { + const [res, error] = await finalizeIdeationAction({ + teamId, + ideationId: finalizedIdeation.id, + }); + + if (res) { + router.push(routePaths.ideationPage(teamId.toString())); + } + + if (error) { + dispatch( + onOpenModal({ type: "error", content: { message: error.message } }), + ); + } + + setFinalizeIdeationLoading(false); + } + + function renderButtonContent() { + if (finalizeIdeationLoading) { + return ; + } + + return "Finalize Project Idea Selection"; + } + + return ( + + ); +} diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/finalize/components/FinalizeIdeationItem.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/finalize/components/FinalizeIdeationItem.tsx index eed320e3..85b5f339 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/finalize/components/FinalizeIdeationItem.tsx +++ b/src/app/(main)/my-voyage/[teamId]/ideation/finalize/components/FinalizeIdeationItem.tsx @@ -1,30 +1,36 @@ -import { Dispatch, SetStateAction } from "react"; +import { type Dispatch, type SetStateAction } from "react"; import { CheckCircleIcon } from "@heroicons/react/24/outline"; +import { type FinalizedIdeation } from "./FinalizeIdeationList"; import Button from "@/components/Button"; import Avatar from "@/components/avatar/Avatar"; import AvatarGroup from "@/components/avatar/AvatarGroup"; -import { ProjectIdeaVotes } from "@/store/features/ideation/ideationSlice"; +import { type ProjectIdeaVotes } from "@/store/features/ideation/ideationSlice"; interface FinalizeIdeationItemProps { title: string; projectIdeaVotes: ProjectIdeaVotes[]; - finalizedIdeation: string; - setFinalizedIdeation: Dispatch>; + ideationId: number; + finalizedIdeation: FinalizedIdeation; + setFinalizedIdeation: Dispatch>; } export default function FinalizeIdeationItem({ title, projectIdeaVotes, finalizedIdeation, + ideationId, setFinalizedIdeation, }: FinalizeIdeationItemProps) { function handleClick() { - setFinalizedIdeation(title); + setFinalizedIdeation({ + id: ideationId, + title, + }); } return ( + diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/finalize/page.tsx b/src/app/(main)/my-voyage/[teamId]/ideation/finalize/page.tsx index 0e054791..e21ef751 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/finalize/page.tsx +++ b/src/app/(main)/my-voyage/[teamId]/ideation/finalize/page.tsx @@ -1,11 +1,37 @@ +import { redirect } from "next/navigation"; import FinalizeIdeationBanner from "./components/FinalizeIdeationBanner"; import FinalizeIdeationList from "./components/FinalizeIdeationList"; +import { getUser } from "@/utils/getUser"; +import { getCurrentVoyageTeam } from "@/utils/getCurrentVoyageTeam"; +import routePaths from "@/utils/routePaths"; -export default function page() { - return ( -
    - - -
    - ); +interface FinalizeIdeationPageProps { + params: { + teamId: string; + }; +} + +export default async function FinalizeIdeationPage({ + params, +}: FinalizeIdeationPageProps) { + const teamId = Number(params.teamId); + + const [user, error] = await getUser(); + + const { currentTeam, err } = getCurrentVoyageTeam({ user, error, teamId }); + + if (err) { + return err; + } + + if (currentTeam) { + return ( +
    + + +
    + ); + } + + redirect(routePaths.dashboardPage()); } diff --git a/src/app/(main)/my-voyage/[teamId]/ideation/ideationService.ts b/src/app/(main)/my-voyage/[teamId]/ideation/ideationService.ts index 1dc50ae6..63605894 100644 --- a/src/app/(main)/my-voyage/[teamId]/ideation/ideationService.ts +++ b/src/app/(main)/my-voyage/[teamId]/ideation/ideationService.ts @@ -3,7 +3,7 @@ import { revalidateTag } from "next/cache"; import { getAccessToken } from "@/utils/getCookie"; import { DELETE, PATCH, POST } from "@/utils/requests"; -import { AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; +import { type AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; import { CacheTag } from "@/utils/cacheTag"; interface IdeationProps { @@ -36,13 +36,16 @@ export interface DeleteIdeationProps extends IdeationProps {} export interface IdeationVoteProps extends IdeationProps {} export type FetchIdeationsProps = Pick; -export interface AddIdeationResponse extends IdeationResponse { - title: string; - description: string; - vision: string; -} +export interface FinalizeIdeationProps extends IdeationProps {} + +export interface AddIdeationResponse extends IdeationResponse, IdeationBody {} export interface EditIdeationResponse extends AddIdeationResponse {} export interface DeleteIdeationResponse extends AddIdeationResponse {} +export interface FinalizeIdeationResponse + extends IdeationResponse, + IdeationBody { + isSelected: boolean; +} export interface IdeationVoteResponse extends IdeationResponse { projectIdeaId: number; @@ -163,3 +166,27 @@ export async function removeIdeationVote({ return [res, error]; } + +export async function finalizeIdeation({ + teamId, + ideationId, +}: FinalizeIdeationProps): Promise< + AsyncActionResponse +> { + const token = getAccessToken(); + + const finalizeIdeationAsync = () => + POST( + `api/v1/voyages/teams/${teamId}/ideations/${ideationId}/select`, + token, + "default", + ); + + const [res, error] = await handleAsync(finalizeIdeationAsync); + + if (res) { + revalidateTag(CacheTag.ideation); + } + + return [res, error]; +} diff --git a/src/app/(main)/my-voyage/[teamId]/sprints/components/EmptySprintWrapper.tsx b/src/app/(main)/my-voyage/[teamId]/sprints/components/EmptySprintWrapper.tsx index 062afe11..d8c720b1 100644 --- a/src/app/(main)/my-voyage/[teamId]/sprints/components/EmptySprintWrapper.tsx +++ b/src/app/(main)/my-voyage/[teamId]/sprints/components/EmptySprintWrapper.tsx @@ -9,7 +9,7 @@ import Banner from "@/components/banner/Banner"; import EmptySprintProvider from "@/myVoyage/sprints/providers/EmptySprintProvider"; import { getCurrentSprint } from "@/utils/getCurrentSprint"; -import { Sprint } from "@/store/features/sprint/sprintSlice"; +import { type Voyage, type Sprint } from "@/store/features/sprint/sprintSlice"; import { getUser } from "@/utils/getUser"; import { getCurrentVoyageData } from "@/utils/getCurrentVoyageData"; import routePaths from "@/utils/routePaths"; @@ -35,7 +35,7 @@ export default async function EmptySprintWrapper({ const teamId = Number(params.teamId); const sprintNumber = Number(params.sprintNumber); - let sprintsData: Sprint[] = []; + let voyageData: Voyage; const [user, error] = await getUser(); @@ -57,16 +57,16 @@ export default async function EmptySprintWrapper({ if (error) { return `Error: ${error.message}`; } - sprintsData = res!.voyage.sprints; + voyageData = res!; } else { redirect(routePaths.dashboardPage()); } // Check if a meeting exists - const meeting = getMeeting(sprintsData, sprintNumber); + const meeting = getMeeting(voyageData.sprints, sprintNumber); // Get current sprint number - const { number } = getCurrentSprint(sprintsData) as Sprint; + const { number } = getCurrentSprint(voyageData.sprints) as Sprint; const currentSprintNumber = number; // Redirect if a user tries to access a sprint which hasn't started yet @@ -93,13 +93,10 @@ export default async function EmptySprintWrapper({ width="w-[276px]" /> - + - +

    ); } diff --git a/src/app/(main)/my-voyage/[teamId]/sprints/components/ProgressStepper.tsx b/src/app/(main)/my-voyage/[teamId]/sprints/components/ProgressStepper.tsx index 043b480c..89ef4b78 100644 --- a/src/app/(main)/my-voyage/[teamId]/sprints/components/ProgressStepper.tsx +++ b/src/app/(main)/my-voyage/[teamId]/sprints/components/ProgressStepper.tsx @@ -2,7 +2,7 @@ import { useParams, useRouter } from "next/navigation"; import { RocketLaunchIcon } from "@heroicons/react/24/outline"; -import Stepper, { SteppersItem } from "@/components/Stepper"; +import Stepper, { type SteppersItem } from "@/components/Stepper"; import { useSprint } from "@/store/hooks"; import routePaths from "@/utils/routePaths"; @@ -16,10 +16,18 @@ function getStatus(sprintNumber: number, currentSprintNumber: number) { } } -export default function ProgressStepper() { +interface ProgressStepperProps { + currentSprintNumber: number; +} + +export default function ProgressStepper({ + currentSprintNumber, +}: ProgressStepperProps) { const router = useRouter(); const params = useParams<{ teamId: string; sprintNumber: string }>(); - const { currentSprintNumber, sprints } = useSprint(); + const { + voyage: { sprints }, + } = useSprint(); function handleClick(sprintNumber: number) { const meetingId = sprints.find((sprint) => sprint.number === sprintNumber)! diff --git a/src/app/(main)/my-voyage/[teamId]/sprints/components/RedirectToCurrentSprintWrapper.tsx b/src/app/(main)/my-voyage/[teamId]/sprints/components/RedirectToCurrentSprintWrapper.tsx index 47eb2d7b..4cd1ad59 100644 --- a/src/app/(main)/my-voyage/[teamId]/sprints/components/RedirectToCurrentSprintWrapper.tsx +++ b/src/app/(main)/my-voyage/[teamId]/sprints/components/RedirectToCurrentSprintWrapper.tsx @@ -1,9 +1,9 @@ import { redirect } from "next/navigation"; import { - FetchSprintsProps, - FetchSprintsResponse, - SprintsResponse, + type FetchSprintsProps, + type FetchSprintsResponse, + type SprintsResponse, } from "@/myVoyage/sprints/sprintsService"; import { getAccessToken } from "@/utils/getCookie"; @@ -11,8 +11,8 @@ import { getUser } from "@/utils/getUser"; import { getCurrentSprint } from "@/utils/getCurrentSprint"; import { GET } from "@/utils/requests"; import { CacheTag } from "@/utils/cacheTag"; -import { AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; -import { Sprint } from "@/store/features/sprint/sprintSlice"; +import { type AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; +import { type Sprint } from "@/store/features/sprint/sprintSlice"; import { getCurrentVoyageData } from "@/utils/getCurrentVoyageData"; import routePaths from "@/utils/routePaths"; @@ -25,7 +25,7 @@ export async function fetchSprints({ `api/v1/voyages/sprints/teams/${teamId}`, token, "force-cache", - CacheTag.sprints + CacheTag.sprints, ); return await handleAsync(fetchSprintsAsync); @@ -65,15 +65,13 @@ export default async function RedirectToCurrentSprintWrapper({ if (error) { return `Error: ${error.message}`; } - const { teamMeetings, number } = getCurrentSprint( - res!.voyage.sprints - ) as Sprint; + const { teamMeetings, number } = getCurrentSprint(res!.sprints) as Sprint; currentSprintNumber = number; currentMeetingId = teamMeetings[0]?.id; if (currentMeetingId) { redirect( - `/my-voyage/${teamId}/sprints/${currentSprintNumber}/meeting/${currentMeetingId}` + `/my-voyage/${teamId}/sprints/${currentSprintNumber}/meeting/${currentMeetingId}`, ); } else { redirect(`/my-voyage/${teamId}/sprints/${currentSprintNumber}`); diff --git a/src/app/(main)/my-voyage/[teamId]/sprints/components/SprintWrapper.tsx b/src/app/(main)/my-voyage/[teamId]/sprints/components/SprintWrapper.tsx index 792f33a8..7715db5a 100644 --- a/src/app/(main)/my-voyage/[teamId]/sprints/components/SprintWrapper.tsx +++ b/src/app/(main)/my-voyage/[teamId]/sprints/components/SprintWrapper.tsx @@ -12,19 +12,27 @@ import VoyagePageBannerContainer from "@/components/banner/VoyagePageBannerConta import Banner from "@/components/banner/Banner"; import { - FetchMeetingProps, - FetchMeetingResponse, + type FetchMeetingProps, + type FetchMeetingResponse, } from "@/myVoyage/sprints/sprintsService"; -import { Agenda, Meeting, Sprint } from "@/store/features/sprint/sprintSlice"; + +import { + type Agenda, + type Meeting, + type Sprint, + type Section, + type Voyage, +} from "@/store/features/sprint/sprintSlice"; import { getCurrentSprint } from "@/utils/getCurrentSprint"; -import { AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; +import { type AsyncActionResponse, handleAsync } from "@/utils/handleAsync"; import { GET } from "@/utils/requests"; import { getAccessToken } from "@/utils/getCookie"; import { getUser } from "@/utils/getUser"; import { getSprintCache } from "@/utils/getSprintCache"; import { getCurrentVoyageData } from "@/utils/getCurrentVoyageData"; import routePaths from "@/utils/routePaths"; +import { SprintSections } from "@/utils/sections"; async function fetchMeeting({ sprintNumber, @@ -37,7 +45,7 @@ async function fetchMeeting({ `api/v1/voyages/sprints/meetings/${meetingId}`, token, "force-cache", - sprintCache + sprintCache, ); return await handleAsync(fetchMeetingAsync); @@ -56,9 +64,10 @@ export default async function SprintWrapper({ params }: SprintWrapperProps) { const sprintNumber = Number(params.sprintNumber); const meetingId = Number(params.meetingId); - let sprintsData: Sprint[] = []; + let voyageData: Voyage; let meetingData: Meeting = { id: +params.meetingId }; let agendaData: Agenda[] = []; + let sectionsData: Section[] = []; const [user, error] = await getUser(); @@ -80,13 +89,13 @@ export default async function SprintWrapper({ params }: SprintWrapperProps) { if (error) { return `Error: ${error.message}`; } - sprintsData = res!.voyage.sprints; + voyageData = res!; } else { redirect(routePaths.dashboardPage()); } - const correspondingMeetingId = sprintsData.find( - (sprint) => sprint.number === sprintNumber + const correspondingMeetingId = voyageData.sprints.find( + (sprint) => sprint.number === sprintNumber, )?.teamMeetings[0]?.id; if (meetingId === correspondingMeetingId) { @@ -95,6 +104,9 @@ export default async function SprintWrapper({ params }: SprintWrapperProps) { if (res) { meetingData = res; agendaData = res.agendas; + if (res.formResponseMeeting.length !== 0) { + sectionsData = res.formResponseMeeting; + } } else { return `Error: ${error?.message}`; } @@ -103,7 +115,7 @@ export default async function SprintWrapper({ params }: SprintWrapperProps) { } // Get current sprint number - const { number } = getCurrentSprint(sprintsData) as Sprint; + const { number } = getCurrentSprint(voyageData.sprints) as Sprint; const currentSprintNumber = number; // Redirect if a user tries to access a sprint which hasn't started yet @@ -126,21 +138,26 @@ export default async function SprintWrapper({ params }: SprintWrapperProps) { /> - + - + - + section.form.id === Number(SprintSections.planning), + )} + review={sectionsData.find( + (section) => section.form.id === Number(SprintSections.review), + )} + />
    ); } diff --git a/src/app/(main)/my-voyage/[teamId]/sprints/components/agenda/AgendaTopic.tsx b/src/app/(main)/my-voyage/[teamId]/sprints/components/agenda/AgendaTopic.tsx index d6489bad..03c5d8ef 100644 --- a/src/app/(main)/my-voyage/[teamId]/sprints/components/agenda/AgendaTopic.tsx +++ b/src/app/(main)/my-voyage/[teamId]/sprints/components/agenda/AgendaTopic.tsx @@ -6,7 +6,7 @@ import { import { CheckCircleIcon as CheckCircleIconSolid } from "@heroicons/react/24/solid"; import IconButton from "@/components/IconButton"; -import { Agenda } from "@/app/(main)/my-voyage/[teamId]/sprints/components/fixtures/Meeting"; +import { type Agenda } from "@/app/(main)/my-voyage/[teamId]/sprints/components/fixtures/Meeting"; import { cn } from "@/lib/utils"; interface TopicProps { diff --git a/src/app/(main)/my-voyage/[teamId]/sprints/components/agenda/Agendas.tsx b/src/app/(main)/my-voyage/[teamId]/sprints/components/agenda/Agendas.tsx index ef740d3c..60e92309 100644 --- a/src/app/(main)/my-voyage/[teamId]/sprints/components/agenda/Agendas.tsx +++ b/src/app/(main)/my-voyage/[teamId]/sprints/components/agenda/Agendas.tsx @@ -10,7 +10,7 @@ import AgendaHeader from "./AgendaHeader"; import routePaths from "@/utils/routePaths"; import Divider from "@/myVoyage/sprints/components/Divider"; -import { Agenda } from "@/store/features/sprint/sprintSlice"; +import { type Agenda } from "@/store/features/sprint/sprintSlice"; import useServerAction from "@/hooks/useServerAction"; import { changeAgendaTopicStatus } from "@/myVoyage/sprints/sprintsService"; import { useAppDispatch } from "@/store/hooks"; diff --git a/src/app/(main)/my-voyage/[teamId]/sprints/components/forms/AgendaTopicForm.tsx b/src/app/(main)/my-voyage/[teamId]/sprints/components/forms/AgendaTopicForm.tsx index 18683d48..6c8367b3 100644 --- a/src/app/(main)/my-voyage/[teamId]/sprints/components/forms/AgendaTopicForm.tsx +++ b/src/app/(main)/my-voyage/[teamId]/sprints/components/forms/AgendaTopicForm.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { type SubmitHandler, useForm } from "react-hook-form"; import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { TrashIcon } from "@heroicons/react/20/solid"; @@ -13,7 +13,7 @@ import Textarea from "@/components/inputs/Textarea"; import { validateTextInput } from "@/helpers/form/validateInput"; import { useSprint, useAppDispatch } from "@/store/hooks"; -import { Agenda } from "@/store/features/sprint/sprintSlice"; +import { type Agenda } from "@/store/features/sprint/sprintSlice"; import useServerAction from "@/hooks/useServerAction"; import { addAgendaTopic, @@ -54,7 +54,9 @@ export default function AgendaTopicForm() { ]; const dispatch = useAppDispatch(); - const { sprints } = useSprint(); + const { + voyage: { sprints }, + } = useSprint(); const [editMode, setEditMode] = useState(false); const [topicData, setTopicData] = useState(); const [saveTimeout, setSaveTimeout] = useState(null); diff --git a/src/app/(main)/my-voyage/[teamId]/sprints/components/forms/MeetingForm.tsx b/src/app/(main)/my-voyage/[teamId]/sprints/components/forms/MeetingForm.tsx index 82235f22..6518c647 100644 --- a/src/app/(main)/my-voyage/[teamId]/sprints/components/forms/MeetingForm.tsx +++ b/src/app/(main)/my-voyage/[teamId]/sprints/components/forms/MeetingForm.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { type SubmitHandler, useForm } from "react-hook-form"; +import { format } from "date-fns-tz"; import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -18,8 +19,8 @@ import { validateDateTimeInput, validateTextInput, } from "@/helpers/form/validateInput"; -import { useSprint, useAppDispatch } from "@/store/hooks"; -import { Meeting } from "@/store/features/sprint/sprintSlice"; +import { useSprint, useAppDispatch, useUser } from "@/store/hooks"; +import { type Meeting } from "@/store/features/sprint/sprintSlice"; import { onOpenModal } from "@/store/features/modal/modalSlice"; import useServerAction from "@/hooks/useServerAction"; import { addMeeting, editMeeting } from "@/myVoyage//sprints/sprintsService"; @@ -27,14 +28,6 @@ import routePaths from "@/utils/routePaths"; import { persistor } from "@/store/store"; import convertStringToDate from "@/utils/convertStringToDate"; -const dateWithoutTimezone = (date: Date) => { - const tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds - const withoutTimezone = new Date(date.valueOf() - tzoffset) - .toISOString() - .slice(0, -1); - return withoutTimezone; -}; - export default function MeetingForm() { const router = useRouter(); const params = useParams<{ @@ -49,7 +42,10 @@ export default function MeetingForm() { ]; const dispatch = useAppDispatch(); - const { sprints } = useSprint(); + const { + voyage: { sprints }, + } = useSprint(); + const { timezone } = useUser(); const [editMode, setEditMode] = useState(false); const [meetingData, setMeetingData] = useState(); const [saveTimeout, setSaveTimeout] = useState(null); @@ -64,13 +60,14 @@ export default function MeetingForm() { required: true, maxLen: 50, }), - notes: validateTextInput({ + description: validateTextInput({ inputName: "Description", required: true, }), dateTime: validateDateTimeInput({ - minDate: convertStringToDate(startDate), - maxDate: convertStringToDate(endDate), + minDate: convertStringToDate(startDate, timezone), + maxDate: convertStringToDate(endDate, timezone), + timezone, }), meetingLink: validateTextInput({ inputName: "Meeting Link", @@ -105,7 +102,7 @@ export default function MeetingForm() { resolver: zodResolver(validationSchema), }); - const { title, notes, dateTime, meetingLink } = watch(); + const { title, description, dateTime, meetingLink } = watch(); const setCustomValue = (id: "dateTime", value: Date) => { setValue(id, value, { @@ -116,7 +113,9 @@ export default function MeetingForm() { }; const onSubmit: SubmitHandler = async (data) => { - const dateTime = dateWithoutTimezone(data.dateTime); + const dateTime = format(data.dateTime, "yyyy-MM-dd HH:mm:ssXXX", { + timeZone: timezone, + }); if (editMode) { const [res, error] = await editMeetingAction({ @@ -182,15 +181,17 @@ export default function MeetingForm() { if (meetingData && meetingData.dateTime) { const dateTimeConvertedToDate = convertStringToDate( meetingData?.dateTime, + timezone, ); + reset({ title: meetingData?.title, - notes: meetingData?.notes, + description: meetingData?.description, meetingLink: meetingData?.meetingLink, dateTime: dateTimeConvertedToDate, }); } - }, [meetingData, reset]); + }, [meetingData, reset, timezone]); useEffect( () => () => { @@ -271,7 +272,7 @@ export default function MeetingForm() { editMeetingAction, setEditMeetingLoading, title, - notes, + description, dateTime, meetingLink, ]); @@ -320,9 +321,9 @@ export default function MeetingForm() { id="description" label="description" placeholder="Please provide a brief description of the goals for this meeting." - {...register("notes")} - errorMessage={errors.notes?.message} - defaultValue={meetingData?.notes ?? ""} + {...register("description")} + errorMessage={errors.description?.message} + defaultValue={meetingData?.description ?? ""} /> {/* Project Name */} -