diff --git a/.github/workflows/amplify-deployment.yml b/.github/workflows/amplify-deployment.yml new file mode 100644 index 0000000..f36b18e --- /dev/null +++ b/.github/workflows/amplify-deployment.yml @@ -0,0 +1,81 @@ +name: Set Amplify Environment Variables and Trigger Deployment + +permissions: + id-token: write + contents: read + +on: + push: + branches: + - main + paths: + - "tasker-client/**" + +jobs: + checkout-code: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + install-cli: + runs-on: ubuntu-latest + needs: checkout-code + steps: + - name: Install AWS CLI + run: | + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install + + - name: Verify AWS CLI Installation + run: aws --version + + - name: Install Amplify CLI + run: | + npm install -g @aws-amplify/cli + + - name: Verify Amplify CLI Installation + run: amplify --version + + assume-role: + runs-on: ubuntu-latest + needs: install-cli + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + deploy-amplify: + runs-on: ubuntu-latest + needs: [checkout-code, assume-role, install-cli] + steps: + - name: Fetch API URL from SSM + id: fetch-ssm + run: | + export NEXT_PUBLIC_API_BASE_URL=$(aws ssm get-parameter --name "/tasker/api/base-url" --query "Parameter.Value" --output text) + export NEXT_PUBLIC_COGNITO_USER_POOL_ID=$(aws ssm get-parameter --name "/tasker/cognito/user-pool-id" --query "Parameter.Value" --output text) + export NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID=$(aws ssm get-parameter --name "/tasker/cognito/client-id" --query "Parameter.Value" --output text) + export NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL=$(aws ssm get-parameter --name "/tasker/s3/public-images-url" --query "Parameter.Value" --output text) + + echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL" >> $GITHUB_ENV + echo "NEXT_PUBLIC_COGNITO_USER_POOL_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_ID" >> $GITHUB_ENV + echo "NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID" >> $GITHUB_ENV + echo "NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL=$NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL" >> $GITHUB_ENV + + - name: Set Amplify Environment Variables + run: | + amplify env set NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL + amplify env set NEXT_PUBLIC_COGNITO_USER_POOL_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_ID + amplify env set NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID + amplify env set NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL=$NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL + + - name: Deploy Amplify App + run: | + export AMPLIFY_MONOREPO_APP_ROOT=tasker-client + export AMPLIFY_DIFF_DEPLOY=false + aws amplify start-deployment \ + --app-id ${{ secrets.AWS_AMPLIFY_APP_ID }} \ + --branch-name main diff --git a/.github/workflows/serverless-deployment.yml b/.github/workflows/serverless-deployment.yml new file mode 100644 index 0000000..50757f8 --- /dev/null +++ b/.github/workflows/serverless-deployment.yml @@ -0,0 +1,69 @@ +name: Deploy with Serverless Framework + +permissions: + id-token: write + contents: read + +on: + push: + branches: + - main + paths: + - "tasker-server/**" + +jobs: + checkout-code: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + install-deps: + runs-on: ubuntu-latest + needs: checkout-code + steps: + - name: Install AWS CLI + run: | + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install + + - name: Verify AWS CLI Installation + run: aws --version + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: "20" + + - name: Install Serverless Framework + run: npm install -g serverless + + run-tests: + runs-on: ubuntu-latest + needs: install-deps + steps: + - name: Run Tests + run: | + cd tasker-server + npm install + npm test:ci + + assume-role: + runs-on: ubuntu-latest + needs: install-deps + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + deploy-serverless: + runs-on: ubuntu-latest + needs: [checkout-code, assume-role, install-deps] + steps: + - name: Deploy Serverless Application + run: | + cd tasker-server + serverless deploy diff --git a/tasker-client/amplify.yml b/tasker-client/amplify.yml new file mode 100644 index 0000000..3145809 --- /dev/null +++ b/tasker-client/amplify.yml @@ -0,0 +1,26 @@ +version: 1.0 +frontend: + phases: + preBuild: + commands: + - npm ci + build: + commands: + - npm run build + artifacts: + baseDirectory: .next + files: + - "**/*" + cache: + paths: + - node_modules/**/* + - .next/cache/**/* + - .npm/**/* +env: + variables: + AMPLIFY_MONOREPO_APP_ROOT: tasker-client + AMPLIFY_DIFF_DEPLOY: false + NEXT_PUBLIC_API_BASE_URL: "" + NEXT_PUBLIC_COGNITO_USER_POOL_ID: "" + NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID: "" + NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL: "" diff --git a/tasker-client/next.config.ts b/tasker-client/next.config.ts index e9ffa30..eede2b9 100644 --- a/tasker-client/next.config.ts +++ b/tasker-client/next.config.ts @@ -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; diff --git a/tasker-client/src/app/components/ModalNewTask/index.tsx b/tasker-client/src/app/components/ModalNewTask/index.tsx index 9748383..0631291 100644 --- a/tasker-client/src/app/components/ModalNewTask/index.tsx +++ b/tasker-client/src/app/components/ModalNewTask/index.tsx @@ -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) => { setAuthorUserId(e.target.value)} - /> + {authorUserId === "" && ( + setAuthorUserId(e.target.value)} + /> + )} {
{!!currentUserDetails?.profilePictureUrl ? ( {currentUserDetails?.username {
logo {
{!!currentUserDetails?.profilePictureUrl ? ( {currentUserDetails?.username {
{task.attachments && task.attachments.length > 0 && ( {task.attachments[0].fileName} {
{user.profilePictureUrl && ( profile picture { data: fetchedTasks, isLoading, error, + refetch, } = useGetTasksQuery({ projectId }); const [updateTaskStatus] = useUpdateTaskStatusMutation(); const [tasks, setTasks] = useState([]); @@ -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 && ( {task.attachments[0].fileName} {
{task.assignee && ( {task.assignee.username} { )} {task.author && ( {task.author.username} { }); 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) => {
-
- -
+ {ganttTasks.length > 0 && ( +
+ +
+ )}