Skip to content

Commit

Permalink
feat: add an ability to create a project gf-87 (#115)
Browse files Browse the repository at this point in the history
* fix: filename gf-87

* feat: improve Button component gf-87

* feat: improve Input component gf-87

* feat: add an ability to create new project gf-87

* fix: disable button while name is empty gf-87

* refactor: move empty string length const into consts folder gf-87

* refactor: cleanup Input component and its styles gf-87

* refactor: rename Button disabled prop and fix its styles gf-87

* fix: correct the order of projects returned by backend gf-87

* fix: close modal only on success gf-87

Also:
- add success toast notification
- fix data rerendering issue

* fix: wrap modal open/close functions with useCallback gf-87

Without useCallback this functions recalculates when modal opens,
which leads to issues when use these functions as dependencies in other hooks

* refactor: rename 'success message' to 'notification message' gf-87

* refactor: rename 'projectsStatus' back to 'dataStatus' gf-87

* fix: button styles gf-87

* refactor: rename EMPTY_STRING_LENGTH constant and make it common gf-87

* refactor: rid of DESCRIPTION_ROWS_COUNT constant gf-87

* refactor: rename 'rows' to 'rowsCount' gf-87

* refactor: move EMPTY_LENGTH to shared package gf-87

* refactor: create SortType enum gf-87

* refactor: rename 'event' to 'event_' gf-87

* fix: make create button disabled only when there is errors in form gf-87

* fix: remove button disalability gf-87

* refactor: make button variant prop inline gf-87

* refactor: move SortType to shared gf-87

* fix: rid of ProjectCreateResponseDto gf-87

---------

Co-authored-by: Yelyzaveta Veis <[email protected]>
  • Loading branch information
GvoFor and liza-veis authored Sep 2, 2024
1 parent 81a991d commit 2e08e17
Show file tree
Hide file tree
Showing 32 changed files with 280 additions and 34 deletions.
1 change: 1 addition & 0 deletions apps/backend/src/libs/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { SortType } from "@git-fit/shared";
export {
APIPath,
AppEnvironment,
Expand Down
6 changes: 5 additions & 1 deletion apps/backend/src/modules/projects/project.repository.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SortType } from "~/libs/enums/enums.js";
import { type Repository } from "~/libs/types/types.js";

import { ProjectEntity } from "./project.entity.js";
Expand Down Expand Up @@ -36,7 +37,10 @@ class ProjectRepository implements Repository {
}

public async findAll(): Promise<ProjectEntity[]> {
const projects = await this.projectModel.query().execute();
const projects = await this.projectModel
.query()
.orderBy("created_at", SortType.DESCENDING)
.execute();

return projects.map((project) => ProjectEntity.initialize(project));
}
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/assets/css/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
--color-success: #0d9c00;
--color-warning: #d3bf06;
--color-danger: #ca4925;
--color-danger-hover: #b83b19;

/* Shadow */
--box-shadow: 0px 0px 25px 0px #00000080;
Expand Down
21 changes: 18 additions & 3 deletions apps/frontend/src/libs/components/button/button.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
import { NavLink } from "~/libs/components/components.js";
import { getValidClassNames } from "~/libs/helpers/helpers.js";

import styles from "./styles.module.css";

type Properties = {
href?: string | undefined;
isDisabled?: boolean;
label: string;
onClick?: () => void;
type?: "button" | "submit";
variant?: "danger" | "default" | "outlined";
};

const Button = ({
href,
isDisabled = false,
isDisabled,
label,
onClick,
type = "button",
variant = "default",
}: Properties): JSX.Element => {
const buttonClassName = getValidClassNames(
styles["button"],
styles[`button-${variant}`],
);

if (href) {
return (
<NavLink className={styles["button"] ?? ""} to={href}>
<NavLink className={buttonClassName} to={href}>
{label}
</NavLink>
);
}

return (
<button className={styles["button"]} disabled={isDisabled} type={type}>
<button
className={buttonClassName}
disabled={isDisabled}
onClick={onClick}
type={type}
>
{label}
</button>
);
Expand Down
18 changes: 18 additions & 0 deletions apps/frontend/src/libs/components/button/styles.module.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.button {
min-height: 44px;
padding: 10px 16px;
font-family: Inter, sans-serif;
font-size: 16px;
Expand All @@ -21,3 +22,20 @@
.button:hover:not(:disabled) {
background-color: var(--color-brand-primary-hover);
}

.button-danger {
background-color: var(--color-danger);
}

.button-danger:hover:not(:disabled) {
background-color: var(--color-danger-hover);
}

.button-outlined {
background-color: transparent;
border: 1px solid var(--color-brand-primary);
}

.button-outlined:hover:not(:disabled) {
background-color: var(--color-background-hover);
}
43 changes: 28 additions & 15 deletions apps/frontend/src/libs/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Properties<T extends FieldValues> = {
name: FieldPath<T>;
placeholder?: string;
rightIcon?: JSX.Element;
rowsCount?: number;
type?: "email" | "password" | "text";
};

Expand All @@ -33,6 +34,7 @@ const Input = <T extends FieldValues>({
name,
placeholder = "",
rightIcon,
rowsCount,
type = "text",
}: Properties<T>): JSX.Element => {
const { field } = useFormController({ control, name });
Expand All @@ -41,11 +43,7 @@ const Input = <T extends FieldValues>({
const hasError = Boolean(error);
const hasLeftIcon = Boolean(leftIcon);
const hasRightIcon = Boolean(rightIcon);
const inputClassNames = getValidClassNames(
styles["input-field"],
hasLeftIcon && styles["with-left-icon"],
hasRightIcon && styles["with-right-icon"],
);
const isTextArea = Boolean(rowsCount);

return (
<label className={styles["input-label"]}>
Expand All @@ -62,16 +60,31 @@ const Input = <T extends FieldValues>({
</div>
)}

<input
autoComplete={autoComplete}
className={inputClassNames}
disabled={isDisabled}
name={field.name}
onChange={field.onChange}
placeholder={placeholder}
type={type}
value={field.value}
/>
{isTextArea ? (
<textarea
className={getValidClassNames(
styles["input-field"],
styles["input-textarea"],
)}
disabled={isDisabled}
name={field.name}
onChange={field.onChange}
placeholder={placeholder}
rows={rowsCount}
value={field.value}
/>
) : (
<input
autoComplete={autoComplete}
className={styles["input-field"]}
disabled={isDisabled}
name={field.name}
onChange={field.onChange}
placeholder={placeholder}
type={type}
value={field.value}
/>
)}

{hasRightIcon && (
<div
Expand Down
15 changes: 15 additions & 0 deletions apps/frontend/src/libs/components/input/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@
border-color: var(--color-brand-primary-hover);
}

.input-textarea {
resize: none;
}

.input-textarea::-webkit-scrollbar {
cursor: default;
background-color: inherit;
}

.input-textarea::-webkit-scrollbar-thumb {
cursor: default;
background-color: var(--color-border-secondary);
border-radius: 6px;
}

.input-icon {
position: absolute;
top: 50%;
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/libs/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { SIDEBAR_ITEMS } from "./navigation-items.constant.js";
export { EMPTY_LENGTH } from "@git-fit/shared";
2 changes: 1 addition & 1 deletion apps/frontend/src/libs/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { AppRoute } from "./app-route.enum.js";
export { DataStatus } from "./data-status.enum.js";
export { NotificationMessage } from "./notification-massage.enum.js";
export { NotificationMessage } from "./notification-message.enum.js";
export { QueryParameterName } from "./query-parameter-name.enum.js";
export {
APIPath,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const NotificationMessage = {
PROJECT_CREATE_SUCCESS: "Project was successfully created",
SUCCESS_PROFILE_UPDATE: "Successfully updated profile information.",
} as const;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "~/libs/hooks/hooks.js";
import { useCallback, useState } from "~/libs/hooks/hooks.js";

type Properties = {
isModalOpened: boolean;
Expand All @@ -9,13 +9,13 @@ type Properties = {
const useModal = (): Properties => {
const [isModalOpened, setIsModalOpened] = useState<boolean>(false);

const handleModalOpen = (): void => {
const handleModalOpen = useCallback(() => {
setIsModalOpened(true);
};
}, []);

const handleModalClose = (): void => {
const handleModalClose = useCallback(() => {
setIsModalOpened(false);
};
}, []);

return {
isModalOpened,
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/modules/projects/libs/types/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {
type ProjectCreateRequestDto,
type ProjectGetAllItemResponseDto,
type ProjectGetAllResponseDto,
} from "@git-fit/shared";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { projectCreateValidationSchema } from "@git-fit/shared";
17 changes: 17 additions & 0 deletions apps/frontend/src/modules/projects/projects-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type Storage } from "~/libs/modules/storage/storage.js";

import { ProjectsApiPath } from "./libs/enums/enums.js";
import {
type ProjectCreateRequestDto,
type ProjectGetAllItemResponseDto,
type ProjectGetAllResponseDto,
} from "./libs/types/types.js";
Expand All @@ -20,6 +21,22 @@ class ProjectApi extends BaseHTTPApi {
super({ baseUrl, http, path: APIPath.PROJECTS, storage });
}

public async create(
payload: ProjectCreateRequestDto,
): Promise<ProjectGetAllItemResponseDto> {
const response = await this.load(
this.getFullEndpoint(ProjectsApiPath.ROOT, {}),
{
contentType: ContentType.JSON,
hasAuth: true,
method: "POST",
payload: JSON.stringify(payload),
},
);

return await response.json<ProjectGetAllItemResponseDto>();
}

public async getAll(): Promise<ProjectGetAllResponseDto> {
const response = await this.load(
this.getFullEndpoint(ProjectsApiPath.ROOT, {}),
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/modules/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const projectApi = new ProjectApi({

export { projectApi };
export {
type ProjectCreateRequestDto,
type ProjectGetAllItemResponseDto,
type ProjectGetAllResponseDto,
} from "./libs/types/types.js";
export { projectCreateValidationSchema } from "./libs/validation-schemas/validation-schemas.js";
export { actions, reducer } from "./slices/projects.js";
18 changes: 17 additions & 1 deletion apps/frontend/src/modules/projects/slices/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createAsyncThunk } from "@reduxjs/toolkit";

import { NotificationMessage } from "~/libs/enums/enums.js";
import { type AsyncThunkConfig } from "~/libs/types/types.js";
import {
type ProjectCreateRequestDto,
type ProjectGetAllItemResponseDto,
type ProjectGetAllResponseDto,
} from "~/modules/projects/projects.js";
Expand All @@ -28,4 +30,18 @@ const loadAll = createAsyncThunk<
return await projectApi.getAll();
});

export { getById, loadAll };
const create = createAsyncThunk<
ProjectGetAllItemResponseDto,
ProjectCreateRequestDto,
AsyncThunkConfig
>(`${sliceName}/create`, async (payload, { extra }) => {
const { projectApi, toastNotifier } = extra;

const response = await projectApi.create(payload);

toastNotifier.showSuccess(NotificationMessage.PROJECT_CREATE_SUCCESS);

return response;
});

export { create, getById, loadAll };
14 changes: 13 additions & 1 deletion apps/frontend/src/modules/projects/slices/project.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import { DataStatus } from "~/libs/enums/enums.js";
import { type ValueOf } from "~/libs/types/types.js";
import { type ProjectGetAllItemResponseDto } from "~/modules/projects/projects.js";

import { getById, loadAll } from "./actions.js";
import { create, getById, loadAll } from "./actions.js";

type State = {
dataStatus: ValueOf<typeof DataStatus>;
project: null | ProjectGetAllItemResponseDto;
projectCreateStatus: ValueOf<typeof DataStatus>;
projects: ProjectGetAllItemResponseDto[];
projectStatus: ValueOf<typeof DataStatus>;
};

const initialState: State = {
dataStatus: DataStatus.IDLE,
project: null,
projectCreateStatus: DataStatus.IDLE,
projects: [],
projectStatus: DataStatus.IDLE,
};
Expand Down Expand Up @@ -44,6 +46,16 @@ const { actions, name, reducer } = createSlice({
state.projects = [];
state.dataStatus = DataStatus.REJECTED;
});
builder.addCase(create.pending, (state) => {
state.projectCreateStatus = DataStatus.PENDING;
});
builder.addCase(create.fulfilled, (state, action) => {
state.projects = [action.payload, ...state.projects];
state.projectCreateStatus = DataStatus.FULFILLED;
});
builder.addCase(create.rejected, (state) => {
state.projectCreateStatus = DataStatus.REJECTED;
});
},
initialState,
name: "projects",
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/modules/projects/slices/projects.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { getById, loadAll } from "./actions.js";
import { create, getById, loadAll } from "./actions.js";
import { actions } from "./project.slice.js";

const allActions = {
...actions,
create,
getById,
loadAll,
};
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/pages/projects/components/components.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { ProjectCard } from "./project-card/project-card.js";
export { ProjectCreateForm } from "./project-create-form/project-create-form.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DEFAULT_PROJECT_CREATE_PAYLOAD } from "./default-project-create-payload.constant.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type ProjectCreateRequestDto } from "~/modules/projects/projects.js";

const DEFAULT_PROJECT_CREATE_PAYLOAD: ProjectCreateRequestDto = {
description: "",
name: "",
};

export { DEFAULT_PROJECT_CREATE_PAYLOAD };
Loading

0 comments on commit 2e08e17

Please sign in to comment.