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 bbd8109c..b6473e23 100644 --- a/src/app/(main)/my-voyage/[teamId]/features/components/EditPopover.tsx +++ b/src/app/(main)/my-voyage/[teamId]/features/components/EditPopover.tsx @@ -1,9 +1,8 @@ -import { PencilSquareIcon, TrashIcon } from "@heroicons/react/24/outline"; import { type Dispatch, type SetStateAction } from "react"; -import Button from "@/components/Button"; import { useAppDispatch } from "@/store/hooks"; import { onOpenModal } from "@/store/features/modal/modalSlice"; import { deleteFeature } from "@/myVoyage/features/featuresService"; +import EditMenu from "@/components/EditMenu"; interface EditPopoverProps { setEditMode: Dispatch>; @@ -45,26 +44,5 @@ export default function EditPopover({ ); } - return ( -
- - -
- ); + return ; } diff --git a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/AddVoteBtn.tsx b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/AddVoteBtn.tsx new file mode 100644 index 00000000..cfc70adc --- /dev/null +++ b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/AddVoteBtn.tsx @@ -0,0 +1,11 @@ +import Button from "@/components/Button"; + +export default function AddVoteBtn() { + return ( +
+ +
+ ); +} diff --git a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/GetIcons.tsx b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/GetIcons.tsx new file mode 100644 index 00000000..8496c5dd --- /dev/null +++ b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/GetIcons.tsx @@ -0,0 +1,29 @@ +import { + ComputerDesktopIcon, + SwatchIcon, + CodeBracketSquareIcon, + ChartPieIcon, + CloudIcon, + ServerStackIcon, +} from "@heroicons/react/24/solid"; + +export default function GetIcon(cardTitle: string) { + if (cardTitle === "Frontend") { + return ; + } + if (cardTitle === "CSS Library") { + return ; + } + if (cardTitle === "Backend") { + return ; + } + if (cardTitle === "Project Management") { + return ; + } + if (cardTitle === "Cloud Provider") { + return ; + } + if (cardTitle === "Hosting") { + return ; + } +} diff --git a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/RemoveVoteBtn.tsx b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/RemoveVoteBtn.tsx new file mode 100644 index 00000000..6f4136f4 --- /dev/null +++ b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/RemoveVoteBtn.tsx @@ -0,0 +1,53 @@ +import type { Dispatch, SetStateAction } from "react"; +import { EllipsisVerticalIcon } from "@heroicons/react/24/solid"; +import SettingsMenu from "./SettingsMenu"; +import Button from "@/components/Button"; + +interface RemoveVoteBtnProps { + id: number; + openMenu: (value: number) => void; + numberOfVotes: number; + closeMenu: () => void; + setIsEditing: Dispatch>; + isMenuOpen: number; +} + +export default function RemoveVoteBtn({ + id, + openMenu, + numberOfVotes, + closeMenu, + setIsEditing, + isMenuOpen, +}: RemoveVoteBtnProps) { + const handleClick = () => { + openMenu(id); + }; + + return ( +
+ {numberOfVotes < 2 && ( +
+ + {isMenuOpen === id && ( + + )} +
+ )} + +
+ ); +} diff --git a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/SettingsMenu.tsx b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/SettingsMenu.tsx new file mode 100644 index 00000000..12c3bd0a --- /dev/null +++ b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/SettingsMenu.tsx @@ -0,0 +1,39 @@ +"use client"; +import { useEffect, useRef } from "react"; +import EditMenu from "@/components/EditMenu"; + +interface SettingsMenuProps { + onClose: () => void; + setIsEditing: (value: number) => void; + id: number; +} + +export default function SettingsMenu({ + onClose, + setIsEditing, + id, +}: SettingsMenuProps) { + const menuRef = useRef(null); + + const openEdit = () => { + setIsEditing(id); + }; + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + onClose(); + } + } + document.body.addEventListener("click", handleClickOutside); + return () => { + document.body.removeEventListener("click", handleClickOutside); + }; + }, [onClose]); + + return ( +
+ +
+ ); +} diff --git a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackCard.tsx b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackCard.tsx index f623f576..67778b21 100644 --- a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackCard.tsx +++ b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackCard.tsx @@ -1,57 +1,175 @@ -import { PlusCircleIcon } from "@heroicons/react/24/outline"; -import type { TechItem } from "./fixtures/TechStack"; -import myAvatar from "@/public/img/avatar.png"; +"use client"; +import { useRef, useState, useEffect } from "react"; +import type { FormEvent } from "react"; +import GetIcon from "./GetIcons"; +import AddVoteBtn from "./AddVoteBtn"; +import RemoveVoteBtn from "./RemoveVoteBtn"; +import TextInput from "@/components/inputs/TextInput"; import AvatarGroup from "@/components/avatar/AvatarGroup"; import Avatar from "@/components/avatar/Avatar"; import Button from "@/components/Button"; +import type { TechStackItem } from "@/store/features/techStack/techStackSlice"; +import { useUser } from "@/store/hooks"; interface TechStackCardProps { title: string; - data: TechItem[]; + data: TechStackItem[]; } export default function TechStackCard({ title, data }: TechStackCardProps) { + const [isInput, setIsInput] = useState(false); + const [isEditing, setIsEditing] = useState(-1); + const [isDuplicate, setIsDuplicate] = useState(false); + const inputRef = useRef(null); + const editRef = useRef(null); + const items = data.map((item) => item.name.toLowerCase()); + const userId = useUser().id; + const [openMenuId, setOpenMenuId] = useState(-1); + + const toggleAddItemInput = () => { + setIsInput(!isInput); + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + }; + + const handleSettingsMenuClose = () => { + setOpenMenuId(-1); + }; + + const clearActionEditItem = () => { + setIsEditing(-1); + setOpenMenuId(-1); + }; + + const clearActionAdditem = () => { + setIsInput(!isInput); + }; + + const handleOnChange = () => { + const addingItemValue = inputRef.current?.value.toLowerCase(); + + const isDuplicateInAdding = + addingItemValue && items.includes(addingItemValue); + + if (addingItemValue && isDuplicateInAdding !== isDuplicate) { + setIsDuplicate(!!isDuplicateInAdding); + } + }; + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + inputRef.current && + !inputRef.current.contains(event.target as Node) + ) { + setIsInput(false); + } + if (editRef.current && !editRef.current.contains(event.target as Node)) { + setIsEditing(-1); + setOpenMenuId(-1); + } + } + document.body.addEventListener("click", handleClickOutside); + return () => { + document.body.removeEventListener("click", handleClickOutside); + }; + }, [isInput, isEditing]); + return ( -
-
+
+
+ {GetIcon(title)}

{title}

+
    {data.map((element) => (
  • - {element.value} - - {element.users.map((user) => ( - + - ))} - - + + )} + + {isEditing !== element.id && ( + <> + {/*item name*/} +

    + {element.name} +

    + {/*Avatars of voters*/} +
    + + {element.teamTechStackItemVotes.map((vote) => ( + + ))} + +
    + {element.teamTechStackItemVotes + .map((item) => item.votedBy.member.id) + .includes(userId) ? ( + + ) : ( + + )} + + )}
  • ))}
- + + {isInput ? ( +
+ + + ) : ( +
+ +
+ )}
); } diff --git a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackComponentWrapper.tsx b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackComponentWrapper.tsx new file mode 100644 index 00000000..802974e2 --- /dev/null +++ b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackComponentWrapper.tsx @@ -0,0 +1,87 @@ +import { redirect } from "next/navigation"; +import TechStackContainer from "./TechStackContainer"; +import TechStackProvider from "./TechStackProvider"; +import { getAccessToken } from "@/utils/getCookie"; +import { CacheTag } from "@/utils/cacheTag"; +import { handleAsync } from "@/utils/handleAsync"; +import { GET } from "@/utils/requests"; +import { getCurrentVoyageData } from "@/utils/getCurrentVoyageData"; +import { getUser } from "@/utils/getUser"; +import type { TechStackData } from "@/store/features/techStack/techStackSlice"; +import VoyagePageBannerContainer from "@/components/banner/VoyagePageBannerContainer"; +import Banner from "@/components/banner/Banner"; +import routePaths from "@/utils/routePaths"; + +interface FetchTechStackProps { + teamId: number; +} +interface TechStackComponentWrapperProps { + params: { + teamId: string; + }; +} + +export async function fetchTechStack({ teamId }: FetchTechStackProps) { + const token = getAccessToken(); + + const fetchTechStackAsync = () => + GET( + `api/v1/voyages/teams/${teamId}/techs`, + token, + "force-cache", + CacheTag.techStack, + ); + + return await handleAsync(fetchTechStackAsync); +} + +export default async function TechStackComponentWrapper({ + params, +}: TechStackComponentWrapperProps) { + let techStackData: TechStackData[] = []; + const teamId = Number(params.teamId); + + const [user, error] = await getUser(); + + const { errorResponse, data } = await getCurrentVoyageData({ + user, + error, + teamId, + args: { teamId }, + func: fetchTechStack, + }); + + if (errorResponse) { + return errorResponse; + } + if (data) { + const [res, error] = data; + + if (error) { + return `Error: ${error.message}`; + } + + techStackData = res!; + } else { + redirect(routePaths.dashboardPage()); + } + + return ( + <> + + + + + + + ); +} diff --git a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackContainer.tsx b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackContainer.tsx index 0455544f..75bcb37f 100644 --- a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackContainer.tsx +++ b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackContainer.tsx @@ -1,16 +1,29 @@ import TechStackCard from "./TechStackCard"; -import { techStack } from "./fixtures/TechStack"; +import Button from "@/components/Button"; +import type { TechStackData } from "@/store/features/techStack/techStackSlice"; + +interface TechStackContainerProps { + data: TechStackData[]; +} + +export default function TechStackContainer({ data }: TechStackContainerProps) { + const techCardData = data.map((item) => ({ + id: item.id, + title: item.name, + techItems: item.teamTechStackItems, + })); -export default function TechStackContainer() { return ( -
-
    - {Object.keys(techStack).map((cardType) => ( -
  • - +
    +
    +
    + +
    +
    +
      + {techCardData.map((item) => ( +
    • +
    • ))}
    diff --git a/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackProvider.tsx b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackProvider.tsx new file mode 100644 index 00000000..a6035640 --- /dev/null +++ b/src/app/(main)/my-voyage/[teamId]/tech-stack/components/TechStackProvider.tsx @@ -0,0 +1,19 @@ +"use client"; +import { useEffect } from "react"; +import { fetchTechStack } from "@/store/features/techStack/techStackSlice"; +import type { TechStackData } from "@/store/features/techStack/techStackSlice"; +import { useAppDispatch } from "@/store/hooks"; + +export interface TechStackProviderProps { + payload: TechStackData[]; +} + +export default function TechStackProvider({ payload }: TechStackProviderProps) { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(fetchTechStack(payload)); + }, [dispatch, payload]); + + return null; +} diff --git a/src/app/(main)/my-voyage/[teamId]/tech-stack/page.tsx b/src/app/(main)/my-voyage/[teamId]/tech-stack/page.tsx index cfb0102c..bc76529b 100644 --- a/src/app/(main)/my-voyage/[teamId]/tech-stack/page.tsx +++ b/src/app/(main)/my-voyage/[teamId]/tech-stack/page.tsx @@ -1,23 +1,19 @@ -import TechStackContainer from "./components/TechStackContainer"; -import VoyagePageBannerContainer from "@/components/banner/VoyagePageBannerContainer"; -import Banner from "@/components/banner/Banner"; +import { Suspense } from "react"; +import TechStackComponentWrapper from "./components/TechStackComponentWrapper"; +import Spinner from "@/components/Spinner"; -export default function TeckStackPage() { +interface TechStackPageProps { + params: { + teamId: string; + }; +} + +export default function TeckStackPage({ params }: TechStackPageProps) { return ( <> - - - - + }> + + ); } diff --git a/src/app/(main)/my-voyage/[teamId]/voyage-resources/components/ResourcesComponentWrapper.tsx b/src/app/(main)/my-voyage/[teamId]/voyage-resources/components/ResourcesComponentWrapper.tsx index 9d0717be..13ca6b1b 100644 --- a/src/app/(main)/my-voyage/[teamId]/voyage-resources/components/ResourcesComponentWrapper.tsx +++ b/src/app/(main)/my-voyage/[teamId]/voyage-resources/components/ResourcesComponentWrapper.tsx @@ -10,6 +10,7 @@ import { handleAsync } from "@/utils/handleAsync"; import Banner from "@/components/banner/Banner"; import VoyagePageBannerContainer from "@/components/banner/VoyagePageBannerContainer"; import { getCurrentVoyageData } from "@/utils/getCurrentVoyageData"; +import routePaths from "@/utils/routePaths"; interface FetchResourcesProps { teamId: number; @@ -63,7 +64,7 @@ export default async function ResourcesComponentWrapper({ projectResources = res!; } else { - redirect("/"); + redirect(routePaths.dashboardPage()); } return ( diff --git a/src/components/Button.tsx b/src/components/Button.tsx index bd35bb84..ae7ce753 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -64,6 +64,7 @@ const button = cva( ], }, size: { + xs: ["text-[13px]", "py-1", "px-[18px]"], sm: ["text-[13px]", "py-2.5", "px-[18px]"], md: ["text-[13px]", "py-3", "px-5"], lg: ["text-base", "py-[14px]", "px-[22px]"], diff --git a/src/components/EditMenu.tsx b/src/components/EditMenu.tsx new file mode 100644 index 00000000..597968e5 --- /dev/null +++ b/src/components/EditMenu.tsx @@ -0,0 +1,33 @@ +import { PencilSquareIcon, TrashIcon } from "@heroicons/react/24/outline"; +import Button from "@/components/Button"; + +interface EditMenuProps { + handleClick: () => void; + //TODO: handleDelete will be changed to required. + handleDelete?: () => void; +} + +export default function EditMenu({ handleClick, handleDelete }: EditMenuProps) { + return ( +
    + + +
    + ); +} diff --git a/src/components/avatar/Avatar.tsx b/src/components/avatar/Avatar.tsx index 6029e972..f8dc94f6 100644 --- a/src/components/avatar/Avatar.tsx +++ b/src/components/avatar/Avatar.tsx @@ -18,7 +18,7 @@ export default function Avatar({ return (
    diff --git a/src/components/avatar/AvatarGroup.tsx b/src/components/avatar/AvatarGroup.tsx index 6940ab9b..767aba81 100644 --- a/src/components/avatar/AvatarGroup.tsx +++ b/src/components/avatar/AvatarGroup.tsx @@ -3,5 +3,9 @@ interface AvatarGroupProps { } export default function AvatarGroup({ children }: AvatarGroupProps) { - return
    {children}
    ; + return ( +
    + {children} +
    + ); } diff --git a/src/store/features/techStack/techStackSlice.ts b/src/store/features/techStack/techStackSlice.ts new file mode 100644 index 00000000..f1e8daa6 --- /dev/null +++ b/src/store/features/techStack/techStackSlice.ts @@ -0,0 +1,39 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import type { VoyageMember } from "@/store/features/ideation/ideationSlice"; + +export interface TechStackData { + id: number; + name: string; + description: string; + teamTechStackItems: TechStackItem[]; +} +export interface TechStackItem { + id: number; + name: string; + teamTechStackItemVotes: TechStackItemVotes[]; +} +interface TechStackItemVotes { + votedBy: { + member: VoyageMember; + }; +} +interface TechStackState { + techStack: TechStackData[]; +} +const initialState: TechStackState = { + techStack: [], +}; + +export const techStackSlice = createSlice({ + name: "tech-stack", + initialState, + reducers: { + fetchTechStack: (state, action: PayloadAction) => { + state.techStack = action.payload; + }, + }, +}); + +export const { fetchTechStack } = techStackSlice.actions; +export default techStackSlice.reducer; diff --git a/src/utils/cacheTag.ts b/src/utils/cacheTag.ts index f21bf48b..8f7ff7f5 100644 --- a/src/utils/cacheTag.ts +++ b/src/utils/cacheTag.ts @@ -7,6 +7,7 @@ export enum CacheTag { me = "me", features = "features", resources = "resources", + techStack = "tech-stack", sprints = "sprints", sprint1 = "sprint-1", sprint2 = "sprint-2",