Config ci cd (#5)

* feat: Add GitHub Actions workflow for Amplify deployment and configuration

* feat: Update Amplify deployment workflow and add Serverless deployment workflow

* feat: Set environment variables for Amplify monorepo deployment

* feat: Refactor Amplify deployment configuration for monorepo structure

* feat: Update environment variable naming and refactor image URL handling across the application

* feat: Add Jest configuration and tests for project and user handlers
This commit is contained in:
2024-11-26 16:37:33 +07:00
parent 11e61829f1
commit e293ab3c2c
31 changed files with 4864 additions and 75 deletions

View File

@@ -0,0 +1,35 @@
{
"version": 1,
"applications": [
{
"appRoot": "tasker-client",
"frontend": {
"phases": {
"preBuild": {
"commands": [
"npm ci"
]
},
"build": {
"commands": [
"npm run build"
]
}
},
"artifacts": {
"baseDirectory": ".next",
"files": [
"**/*"
]
},
"cache": {
"paths": [
"node_modules/**/*",
".next/cache/**/*",
".npm/**/*"
]
}
}
}
]
}

View File

@@ -1,7 +1,17 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: "https",
hostname: new URL(process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL || "")
.hostname,
port: "",
pathname: "/**",
},
],
},
};
export default nextConfig;

View File

@@ -1,6 +1,11 @@
import Modal from "@/app/components/Modal";
import { Priority, Status, useCreateTaskMutation } from "@/state/api";
import React, { useState } from "react";
import {
Priority,
Status,
useCreateTaskMutation,
useGetAuthUserQuery,
} from "@/state/api";
import React, { useEffect, useState } from "react";
import { formatISO } from "date-fns";
type Props = {
@@ -22,22 +27,28 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
const [assignedUserId, setAssignedUserId] = useState("");
const [projectId, setProjectId] = useState("");
const { data: currentUser } = useGetAuthUserQuery({});
const userId = currentUser?.userDetails?.userId ?? null;
useEffect(() => {
setAuthorUserId(userId || "");
}, [userId]);
const handleSubmit = async () => {
console.log(title, authorUserId, id, projectId);
if (!(title && authorUserId && (id !== null || projectId))) return;
console.log("Creating task 1..");
if (
!(title && authorUserId && assignedUserId && (id !== null || projectId))
)
return;
console.log("Creating task 2...");
const finalAssignedUserId = assignedUserId.trim() || authorUserId;
const formattedStartDate = formatISO(new Date(startDate), {
representation: "complete",
});
const formattedDueDate = formatISO(new Date(dueDate), {
representation: "complete",
});
const formattedStartDate =
startDate ??
formatISO(new Date(startDate), {
representation: "complete",
});
const formattedDueDate =
dueDate ??
formatISO(new Date(dueDate), {
representation: "complete",
});
await createTask({
title,
@@ -48,16 +59,15 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
startDate: formattedStartDate,
dueDate: formattedDueDate,
authorUserId: authorUserId,
assignedUserId: assignedUserId,
assignedUserId: finalAssignedUserId,
projectId: id !== null ? id : projectId,
});
onClose();
};
const isFormValid = () => {
console.log(title, authorUserId, id, projectId);
return (
title && authorUserId && assignedUserId && (id !== null || projectId)
);
return title && authorUserId && (id !== null || projectId);
};
const selectStyles =
@@ -92,9 +102,13 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
<select
className={selectStyles}
value={status}
onChange={(e) =>
setStatus(Status[e.target.value as keyof typeof Status])
}
onChange={(e) => {
const selectedStatus = Object.entries(Status).find(
([, value]) => value === e.target.value,
)?.[0] as keyof typeof Status;
setStatus(selectedStatus ? Status[selectedStatus] : Status.ToDo);
}}
>
<option value="">Select Status</option>
<option value={Status.ToDo}>To Do</option>
@@ -139,13 +153,15 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
onChange={(e) => setDueDate(e.target.value)}
/>
</div>
<input
type="text"
className={inputStyles}
placeholder="Author User ID"
value={authorUserId}
onChange={(e) => setAuthorUserId(e.target.value)}
/>
{authorUserId === "" && (
<input
type="text"
className={inputStyles}
placeholder="Author User ID"
value={authorUserId}
onChange={(e) => setAuthorUserId(e.target.value)}
/>
)}
<input
type="text"
className={inputStyles}

View File

@@ -77,7 +77,7 @@ const Navbar = () => {
<div className="align-center flex h-9 w-9 justify-center">
{!!currentUserDetails?.profilePictureUrl ? (
<Image
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${currentUserDetails?.profilePictureUrl}`}
src={`${process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL}/${currentUserDetails?.profilePictureUrl}`}
alt={currentUserDetails?.username || "User Profile Picture"}
width={100}
height={50}

View File

@@ -71,7 +71,7 @@ const Sidebar = () => {
</div>
<div className="flex items-center gap-5 border-y-[1.5px] border-gray-200 px-8 py-4 dark:border-gray-700">
<Image
src={`${process.env.S3_PUBLIC_IMAGE_URL}/logo.png`}
src={`${process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL}/logo.png`}
alt="logo"
width={40}
height={40}
@@ -162,7 +162,7 @@ const Sidebar = () => {
<div className="align-center flex h-9 w-9 justify-center">
{!!currentUserDetails?.profilePictureUrl ? (
<Image
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${currentUserDetails?.profilePictureUrl}`}
src={`${process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL}/${currentUserDetails?.profilePictureUrl}`}
alt={currentUserDetails?.username || "User Profile Picture"}
width={100}
height={50}

View File

@@ -16,7 +16,7 @@ const TaskCard = ({ task }: Props) => {
<div className="flex flex-wrap">
{task.attachments && task.attachments.length > 0 && (
<Image
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.attachments[0].fileURL}`}
src={`${process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL}/${task.attachments[0].fileURL}`}
alt={task.attachments[0].fileName}
width={400}
height={200}

View File

@@ -11,7 +11,7 @@ const UserCard = ({ user }: Props) => {
<div className="flex items-center rounded border p-4 shadow">
{user.profilePictureUrl && (
<Image
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${user.profilePictureUrl}`}
src={`${process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL}/${user.profilePictureUrl}`}
alt="profile picture"
width={32}
height={32}

View File

@@ -24,6 +24,7 @@ const BoardView = ({ projectId, setIsModalNewTaskOpen }: BoardProps) => {
data: fetchedTasks,
isLoading,
error,
refetch,
} = useGetTasksQuery({ projectId });
const [updateTaskStatus] = useUpdateTaskStatusMutation();
const [tasks, setTasks] = useState<TaskType[]>([]);
@@ -43,6 +44,7 @@ const BoardView = ({ projectId, setIsModalNewTaskOpen }: BoardProps) => {
try {
await updateTaskStatus({ taskId, status: toStatus });
await refetch();
} catch (error) {
console.error("Failed to update task status:", error);
setTasks(fetchedTasks || []);
@@ -197,7 +199,7 @@ const Task = ({ task }: TaskProps) => {
>
{task.attachments && task.attachments.length > 0 && (
<Image
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.attachments[0].fileURL}`}
src={`${process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL}/${task.attachments[0].fileURL}`}
alt={task.attachments[0].fileName}
width={400}
height={200}
@@ -248,8 +250,8 @@ const Task = ({ task }: TaskProps) => {
<div className="flex -space-x-[6px] overflow-hidden">
{task.assignee && (
<Image
key={task.assignee.userId}
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.assignee.profilePictureUrl!}`}
key={`assignee#${task.assignee.userId}`}
src={`${process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL}/${task.assignee.profilePictureUrl!}`}
alt={task.assignee.username}
width={30}
height={30}
@@ -258,8 +260,8 @@ const Task = ({ task }: TaskProps) => {
)}
{task.author && (
<Image
key={task.author.userId}
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${task.author.profilePictureUrl!}`}
key={`author#${task.author.userId}`}
src={`${process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL}/${task.author.profilePictureUrl!}`}
alt={task.author.username}
width={30}
height={30}

View File

@@ -21,16 +21,19 @@ const Timeline = ({ projectId, setIsModalNewTaskOpen }: Props) => {
});
const ganttTasks = useMemo(() => {
if (!tasks || tasks.length === 0) return [];
return (
tasks?.map((task) => ({
start: new Date(task.startDate as string),
end: new Date(task.dueDate as string),
name: task.title,
id: `Task-${task.taskId}`,
type: "task" as TaskTypeItems,
progress: task.points ? (task.points / 10) * 100 : 0,
isDisabled: false,
})) || []
tasks
?.filter((task) => task.startDate && task.dueDate)
.map((task) => ({
start: new Date(task.startDate as string),
end: new Date(task.dueDate as string),
name: task.title,
id: `Task-${task.taskId}`,
type: "task" as TaskTypeItems,
progress: task.points ? (task.points / 10) * 100 : 0,
isDisabled: false,
})) || []
);
}, [tasks]);
@@ -66,16 +69,20 @@ const Timeline = ({ projectId, setIsModalNewTaskOpen }: Props) => {
</div>
<div className="overflow-hidden rounded-md bg-white shadow dark:bg-dark-secondary dark:text-white">
<div className="timeline">
<Gantt
tasks={ganttTasks}
{...displayOptions}
columnWidth={displayOptions.viewMode === ViewMode.Month ? 150 : 100}
listCellWidth="100px"
barBackgroundColor={isDarkMode ? "#101214" : "#aeb8c2"}
barBackgroundSelectedColor={isDarkMode ? "#000" : "#9ba1a6"}
/>
</div>
{ganttTasks.length > 0 && (
<div className="timeline">
<Gantt
tasks={ganttTasks}
{...displayOptions}
columnWidth={
displayOptions.viewMode === ViewMode.Month ? 150 : 100
}
listCellWidth="100px"
barBackgroundColor={isDarkMode ? "#101214" : "#aeb8c2"}
barBackgroundSelectedColor={isDarkMode ? "#000" : "#9ba1a6"}
/>
</div>
)}
<div className="px-4 pb-5 pt-1">
<button
className="flex items-center rounded bg-blue-primary px-3 py-2 text-white hover:bg-blue-600"

View File

@@ -31,7 +31,7 @@ const columns: GridColDef[] = [
<div className="flex h-full w-full items-center justify-center">
<div className="h-9 w-9">
<Image
src={`${process.env.S3_PUBLIC_IMAGE_URL}/${params.value}`}
src={`${process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL}/${params.value}`}
alt={params.row.username}
width={100}
height={50}