Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/tech stack functionality p2 #144

Merged
merged 28 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b406533
remove padding from Container.
MattRueter Jun 4, 2024
4f39919
create service file.
MattRueter Jun 4, 2024
91364d0
Merge branch 'dev' into feature/tech-stack-functionality-p2
MattRueter Jun 5, 2024
6828911
fix form not connected error.
MattRueter Jun 5, 2024
75d6581
add delete functionality.
MattRueter Jun 6, 2024
9cc804f
add AddtechItem functionality p-1
MattRueter Jun 6, 2024
d8600c3
add editing techItem functionality.
MattRueter Jun 6, 2024
95ed7ad
get users voyageTeamMemberId for use in addTechStack functionality & …
MattRueter Jun 6, 2024
b4a43bd
clean linting errors.
MattRueter Jun 6, 2024
f007140
focus on input when toggled open.
MattRueter Jun 6, 2024
06966ad
Merge branch 'dev' into feature/tech-stack-functionality-p2
Dan-Y-Ko Jun 7, 2024
865798c
move Spinner to buttons.
MattRueter Jun 12, 2024
72cfeb1
change padding on edit form to fix borders.
MattRueter Jun 12, 2024
fa99ad3
Merge branch 'dev' into feature/tech-stack-functionality-p2
MattRueter Jun 12, 2024
3fd99d3
Merge branch 'feature/tech-stack-functionality-p2' of github.com:chin…
MattRueter Jun 12, 2024
b0d09b4
adjust margin of li. & open input with item name as defaultValue.
MattRueter Jun 13, 2024
1ec7bbb
replace boolean check with ternary.
MattRueter Jun 13, 2024
03a21ea
change isEditing to editItemId.
MattRueter Jun 13, 2024
b9bf51c
Merge branch 'dev' into feature/tech-stack-functionality-p2
MattRueter Jun 19, 2024
a679ca6
decouple SettingsMenu and RemoveVoteBtn.
MattRueter Jun 19, 2024
f03759e
get voyageMemberTeamId from getCurrentVoyageTeam util.
MattRueter Jun 19, 2024
dd42c05
comment out unused id for now.
MattRueter Jun 19, 2024
fee2bd0
Merge branch 'dev' into feature/tech-stack-functionality-p2
MattRueter Jun 25, 2024
ba7d92c
create validation schema for adding.
MattRueter Jun 25, 2024
43b21ae
write add tech item validation.
MattRueter Jun 26, 2024
758b276
add editing validation.
MattRueter Jun 26, 2024
523657b
Merge branch 'dev' into feature/tech-stack-functionality-p2
MattRueter Jun 26, 2024
a7eda57
Merge branch 'dev' into feature/tech-stack-functionality-p2
Dan-Y-Ko Jun 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"use client";
import { useEffect, useRef } from "react";
import { deleteTechItem } from "@/myVoyage/tech-stack/techStackService";
import { useAppDispatch } from "@/store/hooks";
import { onOpenModal } from "@/store/features/modal/modalSlice";
import EditMenu from "@/components/EditMenu";

interface SettingsMenuProps {
Expand All @@ -13,12 +16,35 @@ export default function SettingsMenu({
setIsEditing,
id,
}: SettingsMenuProps) {
const dispatch = useAppDispatch();
const menuRef = useRef<HTMLDivElement>(null);

const openEdit = () => {
setIsEditing(id);
};

const handleDelete = () => {
dispatch(
onOpenModal({
type: "confirmation",
content: {
title: "Confirm Deletion",
message:
"Are you sure you want to delete? You will permanently lose all the information and will not be able to recover it.",
confirmationText: "Delete",
cancelText: "Keep It",
},
payload: {
params: {
techItemId: id,
},
redirect: null,
deleteFunction: deleteTechItem,
},
}),
);
};

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
Expand All @@ -33,7 +59,7 @@ export default function SettingsMenu({

return (
<div className="absolute -mt-6 ml-[12px]" ref={menuRef}>
<EditMenu handleClick={openEdit} />
<EditMenu handleClick={openEdit} handleDelete={handleDelete} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
"use client";
import { useRef, useState, useEffect } from "react";
import { useRef, useState, useEffect, useMemo } from "react";
import type { FormEvent } from "react";
import { useParams } from "next/navigation";
import GetIcon from "./GetIcons";
import AddVoteBtn from "./AddVoteBtn";
import RemoveVoteBtn from "./RemoveVoteBtn";
import {
addTechItem,
editTechItem,
} from "@/myVoyage/tech-stack/techStackService";
import getTechCategory from "@/myVoyage/tech-stack/components/getTechCategory";
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";
import { useUser, useAppDispatch, useAppSelector } from "@/store/hooks";
import useServerAction from "@/hooks/useServerAction";
import { onOpenModal } from "@/store/features/modal/modalSlice";
import Spinner from "@/components/Spinner";

interface TechStackCardProps {
title: string;
Expand All @@ -23,15 +32,83 @@ export default function TechStackCard({ title, data }: TechStackCardProps) {
const inputRef = useRef<HTMLInputElement>(null);
const editRef = useRef<HTMLInputElement>(null);
const items = data.map((item) => item.name.toLowerCase());
const params = useParams<{ teamId: string }>();
const teamId = Number(params.teamId);
const userId = useUser().id;

//voyageTeamMembers, currentTeam and voyageTeamMemberId are all used to get
//TODO: create hook or find simpler more readble way to get voyageTeamMemberId?
const voyageTeamMembers = useAppSelector(
(state) => state.user?.voyageTeamMembers || [],
);
const currentTeam = useMemo(
() =>
voyageTeamMembers.filter(
(team) =>
team.voyageTeam.voyage.status.name === "Active" &&
team.voyageTeamId === teamId,
),
[voyageTeamMembers, teamId],
);
const voyageTeamMemberId = currentTeam.length > 0 ? currentTeam[0].id : -1;
Dan-Y-Ko marked this conversation as resolved.
Show resolved Hide resolved

const [openMenuId, setOpenMenuId] = useState(-1);
const techCategoryId = getTechCategory(title) ?? 0;
const dispatch = useAppDispatch();

const {
runAction: addTechItemAction,
isLoading: addTechItemLoading,
setIsLoading: setAddTechItemLoading,
} = useServerAction(addTechItem);

const {
runAction: editTechItemAction,
isLoading: editTechItemLoading,
setIsLoading: setEditTechItemLoading,
} = useServerAction(editTechItem);

const toggleAddItemInput = () => {
setIsInput(!isInput);
};

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
const handleAddItem = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const techName = inputRef.current?.value ?? "";
const [, error] = await addTechItemAction({
teamId,
techName,
techCategoryId,
voyageTeamMemberId,
});
if (error) {
dispatch(
onOpenModal({ type: "error", content: { message: error.message } }),
);
}
setAddTechItemLoading(false);
setIsInput(false);
};

const handleEdit = async (
e: FormEvent<HTMLFormElement>,
techItemId: number,
) => {
e?.preventDefault();
const techName = editRef.current?.value ?? "";
const [, error] = await editTechItemAction({
techItemId,
techName,
});

if (error) {
dispatch(
onOpenModal({ type: "error", content: { message: error.message } }),
);
}
setEditTechItemLoading(false);
setIsEditing(-1);
handleSettingsMenuClose();
};

const handleSettingsMenuClose = () => {
Expand All @@ -49,24 +126,37 @@ export default function TechStackCard({ title, data }: TechStackCardProps) {

const handleOnChange = () => {
const addingItemValue = inputRef.current?.value.toLowerCase();

const isDuplicateInAdding =
addingItemValue && items.includes(addingItemValue);

if (addingItemValue && isDuplicateInAdding !== isDuplicate) {
setIsDuplicate(!!isDuplicateInAdding);
}
};

useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
if (editRef.current) {
editRef.current.focus();
}
}, [isInput, isEditing]);

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
const targetNode = event.target as Node;
if (
inputRef.current &&
!inputRef.current.contains(event.target as Node)
!inputRef.current.contains(targetNode) &&
!(targetNode instanceof HTMLElement && targetNode.closest("form"))
) {
setIsInput(false);
}
if (editRef.current && !editRef.current.contains(event.target as Node)) {
if (
editRef.current &&
!editRef.current.contains(targetNode) &&
!(targetNode instanceof HTMLElement && targetNode.closest("form"))
) {
setIsEditing(-1);
setOpenMenuId(-1);
}
Expand All @@ -78,15 +168,15 @@ export default function TechStackCard({ title, data }: TechStackCardProps) {
}, [isInput, isEditing]);

return (
<div className="h-80 min-w-[420px] rounded-lg bg-base-200 px-6 py-5 text-base-300 sm:w-96">
<div className="h-80 min-w-[420px] rounded-lg bg-base-200 p-5 text-base-300 sm:w-96">
<div className="flex flex-row justify-start">
{GetIcon(title)}
<h3 className="self-center text-xl font-semibold text-base-300">
{title}
</h3>
</div>

<div className="mt-6 h-40 overflow-y-auto pt-1">
<div className="mt-6 h-40 overflow-y-auto p-1">
<ul className="text-base-300">
{data.map((element) => {
const voteIsSubmitted = element.teamTechStackItemVotes.find(
Expand All @@ -98,12 +188,17 @@ export default function TechStackCard({ title, data }: TechStackCardProps) {
key={element.id}
>
{isEditing === element.id && (
Dan-Y-Ko marked this conversation as resolved.
Show resolved Hide resolved
<form className="col-span-6 -my-2 h-12">
<form
onSubmit={(e) => handleEdit(e, element.id)}
className="col-span-6 -my-2 h-12"
>
<TextInput
id={element.id.toString()}
ref={editRef}
placeholder={element.name}
submitButtonText="Save"
submitButtonText={
editTechItemLoading ? <Spinner /> : "Save"
}
clearInputAction={clearActionEditItem}
onChange={handleOnChange}
/>
Expand Down Expand Up @@ -150,12 +245,12 @@ export default function TechStackCard({ title, data }: TechStackCardProps) {
</div>

{isInput ? (
<form onSubmit={handleSubmit}>
<form onSubmit={handleAddItem}>
<TextInput
id={title}
ref={inputRef}
placeholder="Add Tech Stack"
submitButtonText="Save"
submitButtonText={addTechItemLoading ? <Spinner /> : "Save"}
errorMessage={isDuplicate ? "Duplicate Item" : ""}
clearInputAction={clearActionAdditem}
onChange={handleOnChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function TechStackContainer({ data }: TechStackContainerProps) {
}));

return (
<div className="w-full p-10">
<div className="w-full">
<div className="mb-10 grid grid-cols-2 place-items-center min-[1920px]:grid-cols-3">
<div className="col-start-2 flex min-w-[420px] flex-row-reverse sm:w-96 min-[1920px]:col-start-3">
<Button variant="secondary">Finalize Selection</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default function getTechCategory(cardTitle: string) {
if (cardTitle === "Frontend") {
return 1;
}
if (cardTitle === "CSS Library") {
return 2;
}
if (cardTitle === "Backend") {
return 3;
}
if (cardTitle === "Project Management") {
return 4;
}
if (cardTitle === "Cloud Provider") {
return 5;
}
if (cardTitle === "Hosting") {
return 6;
}
}
103 changes: 103 additions & 0 deletions src/app/(main)/my-voyage/[teamId]/tech-stack/techStackService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use server";

import { revalidateTag } from "next/cache";
import { CacheTag } from "@/utils/cacheTag";
import { getAccessToken } from "@/utils/getCookie";
import { type AsyncActionResponse, handleAsync } from "@/utils/handleAsync";
import { DELETE, PATCH, POST } from "@/utils/requests";
import { type TechStackItem } from "@/store/features/techStack/techStackSlice";

interface AddTechItemProps {
teamId: number;
techName: string;
techCategoryId: number;
voyageTeamMemberId: number;
}

type AddTechItemBody = Omit<AddTechItemProps, "teamId">;

interface AddTechItemResponse {
teamTechStackItemVotedId: number;
teamTechId: number;
teamMemberId: number;
createdAt: Date;
updatedAt: Date;
}

interface EditTechItemProps {
techItemId: number;
techName: string;
}

type EditTechItemBody = Omit<EditTechItemProps, "techItemId">;

interface EditTechItemResponse extends TechStackItem {
voyageTeamMemberId: number;
voyageTeamId: number;
}

export interface DeleteTechItemProps {
techItemId: number;
}

export async function addTechItem({
teamId,
techName,
techCategoryId,
voyageTeamMemberId,
}: AddTechItemProps): Promise<AsyncActionResponse<AddTechItemResponse>> {
const token = getAccessToken();
const addTechItemAsync = () =>
POST<AddTechItemBody, AddTechItemResponse>(
`api/v1/voyages/teams/${teamId}/techs`,
token,
"default",
{ techName, techCategoryId, voyageTeamMemberId },
);

const [res, error] = await handleAsync(addTechItemAsync);

if (res) {
revalidateTag(CacheTag.techStack);
}

return [res, error];
}

export async function editTechItem({
techItemId,
techName,
}: EditTechItemProps): Promise<AsyncActionResponse<EditTechItemResponse>> {
const token = getAccessToken();
const editTechItemAsync = () =>
PATCH<EditTechItemBody, EditTechItemResponse>(
`api/v1/voyages/techs/${techItemId}`,
token,
"default",
{ techName },
);

const [res, error] = await handleAsync(editTechItemAsync);

if (res) {
revalidateTag(CacheTag.techStack);
}

return [res, error];
}

export async function deleteTechItem({
techItemId,
}: DeleteTechItemProps): Promise<AsyncActionResponse<void>> {
const token = getAccessToken();

const deleteTechItemAsync = () =>
DELETE<void>(`api/v1/voyages/techs/${techItemId}`, token, "default");
const [res, error] = await handleAsync(deleteTechItemAsync);

if (res) {
revalidateTag(CacheTag.techStack);
}

return [res, error];
}
Loading
Loading