diff --git a/frontend/src/features/AddProject/index.scss b/frontend/src/features/AddProject/index.scss index f1e4f07..f5211d7 100644 --- a/frontend/src/features/AddProject/index.scss +++ b/frontend/src/features/AddProject/index.scss @@ -1,18 +1,25 @@ .add-project-container { - width: 70vw; - height: 76.5vh; - margin: 60px auto; + width: 80% !important; + margin: 60px auto !important; background: linear-gradient(110.51deg, #141432 0.9%, #2a2a4b 101.51%); border-radius: 20px; display: flex; align-items: center; justify-content: center; position: relative; + max-height: 60vh; +} + +.add-project-form { + width: 100%; + padding: 2rem 4rem; + height: 100%; + overflow-y: scroll; } .add-project-form input { box-sizing: border-box; - + width: 100%; /* Auto layout */ border: 0.8px solid #402aa4; border-radius: 14px; @@ -29,7 +36,10 @@ line-height: 30px; /* identical to box height */ - color: rgba(173, 173, 255, 0.75); + color: rgba(173, 173, 255, 0.957); +} +.add-project-form input::placeholder { + color: rgba(173, 173, 255, 0.957); } .input-title { @@ -43,29 +53,17 @@ color: #8181ff; } -.add-project-btn { +.add-project-btnn { position: absolute; right: 66px; - bottom: 44px; - font-family: 'Poppins'; - font-style: normal; - font-weight: 400; - font-size: 15px; - line-height: 22px; - /* identical to box height */ - - display: flex; - align-items: center; - display: flex; - flex-direction: row; - align-items: flex-start; - padding: 10px 16px; - gap: 2px; - flex: none; - order: 1; - flex-grow: 0; + bottom: 14px; + outline: none; + border: none; + padding: 1rem; + border-radius: 2rem; + font-size: 1rem; + padding: 1rem 2rem; background: #402aa4; - border-radius: 14px; color: #ffffff; } @@ -73,3 +71,12 @@ padding-top: 3px; padding-right: 4px; } + +.form-error { + color: red; + font-size: 14px; /* Adjust as necessary */ + word-wrap: break-word; /* Allows breaking long words */ + white-space: normal; /* Ensures normal word wrapping */ + margin-top: 5px; /* Adds some spacing above the error message */ + +} diff --git a/frontend/src/features/AddProject/index.tsx b/frontend/src/features/AddProject/index.tsx index b6d20b7..24e8589 100644 --- a/frontend/src/features/AddProject/index.tsx +++ b/frontend/src/features/AddProject/index.tsx @@ -1,140 +1,194 @@ -import { ChangeEvent, useEffect, useState } from 'react'; import './index.scss'; -import tick from '../../app/assets/images/tick.png'; +import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import toast from 'react-hot-toast'; import { addProject } from 'app/api/project'; -import { Projects, getOrgProjects } from 'app/api/organization'; - +import { Projects, getOrgProjects } from 'app/api/organization'; +import { isGitHubRepositoryLink, isValidName } from './utils'; +import { + _ADD_PROJECT_FORM, + _ADD_PROJECT_FORM_CHANGE, + _ADD_PROJECT_FORM_ERROR, + _FORM_SUBMIT, + _VALIDATE_PROPS, +} from './types'; +import toast from 'react-hot-toast'; +import tick from '../../app/assets/images/tick.png'; const AddProject = () => { const navigate = useNavigate(); const token = localStorage.getItem('token'); const { spaceName } = useParams(); - const [name, setName] = useState(null); - const [description, setDescription] = useState(null); - const [link, setLink] = useState(null); - const [orgProject, setOrgProjects] = useState(null); - const fetchData = async () => { - if (token && spaceName) { - try { - const res = await getOrgProjects(token, spaceName); - setOrgProjects(res.data.projects); - } catch (e) {} + const isUnique = (name: string) => { + if (orgProject && name in orgProject) { + return false; } + return true; }; - useEffect(() => { - fetchData(); - }, [spaceName]); + // form section + const [form, setForm] = useState<_ADD_PROJECT_FORM>({ + name: '', + description: '', + link: '', + }); + const [formErrors, setFormErrors] = useState<_ADD_PROJECT_FORM_ERROR>( + {} as _ADD_PROJECT_FORM_ERROR + ); - const linkChange = async (event: ChangeEvent) => { - setLink(event.target.value); + const validate: _VALIDATE_PROPS = (name, value) => { + switch (name) { + case 'name': + if (!value) { + return 'Name is required'; + } else if (!isValidName(value)) { + return 'Name can only contain alphanumeric characters, hyphens, and underscores'; + } else if (!isUnique(value)) { + return 'Project name already exist'; + } + return ''; + case 'link': + if (!value) { + return 'Project link is required'; + } else if (!isGitHubRepositoryLink(value)) { + return 'Invalid GitHub project link. Ensure it follows the format: https://github.com/username/repo[.git]'; + } + return ''; + case 'description': + if (value.length > 200) { + return 'Description length should not be greater than 200'; + } + return ''; + default: + return ''; + } }; - const nameChange = async (event: ChangeEvent) => { - setName(event.target.value); + const handleChange: _ADD_PROJECT_FORM_CHANGE = (event) => { + const { name, value } = event.target; + setForm({ + ...form, + [name]: value, + }); + }; + const handleBlur = (e: React.FocusEvent) => { + const { name, value } = e.target; + const error = validate(name, value); + setFormErrors({ + ...formErrors, + [name]: error, + }); }; - function isGitHubRepositoryLink(link: string): boolean { - // Define a regular expression pattern for a GitHub repository URL - const githubRepoPattern = - /^https:\/\/github\.com\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+(\.git)?$/; - - // Check if the provided link matches the pattern - return githubRepoPattern.test(link); - } - - function isValidName(input: string): boolean { - // Regular expression to match only alphanumeric characters, hyphens, and underscores - const regex = /^[a-zA-Z0-9-_]+$/; + const handleSubmit: _FORM_SUBMIT = (event) => { + event.preventDefault(); + const newErrors: _ADD_PROJECT_FORM_ERROR = Object.keys(form).reduce( + (acc, key) => { + const error = validate(key, form[key as keyof _ADD_PROJECT_FORM]); + if (error) { + acc[key as keyof _ADD_PROJECT_FORM_ERROR] = error; + } + return acc; + }, + {} as _ADD_PROJECT_FORM_ERROR + ); - // Test if the input string matches the regular expression - return regex.test(input); - } + setFormErrors(newErrors); - const isUnique = (name: string) => { - if (orgProject && name in orgProject) { - return false; + if (Object.values(newErrors).every((error) => !error)) { + if (spaceName && token) { + const func = async () => { + const updatedForm = { ...form }; + if (updatedForm.link && !updatedForm.link.endsWith('.git')) { + updatedForm.link = `${updatedForm.link.trim()}.git`; + } + if (!updatedForm.description) { + updatedForm.description = ' '; + } + const res = await addProject(token, spaceName, updatedForm); + // console.log(res); + // TODO: Update some stuff if the link is case sensitive + navigate(`/workspace/${spaceName}`); + }; + toast.promise(func(), { + loading: 'Saving Project', + success: Project saved, + error: Could not save, + }); + } else { + toast.error('Invalid inputs'); + } + } else { + toast.error('Form contains errors'); } - return true; + console.log('sanas'); }; - const SubmitHandler = async () => { - if ( - spaceName && - token && - name && - link && - isValidName(name) && - isGitHubRepositoryLink(link) && - description && - description?.length < 200 - ) { - const func = async () => { - const res = await addProject(token, spaceName, { - name: name, - description: description, - link: link, - }); - navigate(`/workspace/${spaceName}`); - }; - toast.promise(func(), { - loading: 'Saving Project', - success: Project saved, - error: Could not save, - }); - } else { - toast.error('Invalid inputs'); + const fetchData = async () => { + if (token && spaceName) { + try { + const res = await getOrgProjects(token, spaceName); + setOrgProjects(res.data.projects); + } catch (e) {} } }; + useEffect(() => { + fetchData(); + }, [spaceName]); + return ( -
-
-
-
Name
- - {!name ? 'Name field should not be empty' : <>} - {name && !isValidName(name) && 'Not a valid name'} - {name && !isUnique(name) && 'This project name already exists'} -
Project link
- - {!link ? 'Link field should not be empty' : <>} - {link && - !isGitHubRepositoryLink(link) && - 'Not a valid github repository link'} -
Description
- ) => - setDescription(event.target.value) - } - placeholder='Details about project' - /> - {!description ? 'Description field should not be empty' : <>} - {description && - description.length >= 200 && - 'Description length should not be greater than 200'} -
- -
+
); }; diff --git a/frontend/src/features/AddProject/types.ts b/frontend/src/features/AddProject/types.ts new file mode 100644 index 0000000..5a3fc2f --- /dev/null +++ b/frontend/src/features/AddProject/types.ts @@ -0,0 +1,20 @@ +import { ChangeEvent, FormEvent } from 'react'; + +export type _ADD_PROJECT_FORM_ERROR = { + name: string | null; + description: string | null; + link: string | null; +}; +export type _ADD_PROJECT_FORM = { + name: string; + description: string; + link: string; +}; +export type _ADD_PROJECT_FORM_CHANGE = ( + event: ChangeEvent, + index?: number +) => void; + +export type _FORM_SUBMIT = (event: FormEvent) => void; + +export type _VALIDATE_PROPS = (name: string, value: string) => string; diff --git a/frontend/src/features/AddProject/utils.ts b/frontend/src/features/AddProject/utils.ts new file mode 100644 index 0000000..bfa3530 --- /dev/null +++ b/frontend/src/features/AddProject/utils.ts @@ -0,0 +1,14 @@ +export function isGitHubRepositoryLink(link: string): boolean { + const githubRepoPattern = + /^https:\/\/github\.com\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+$/; + const githubRepoPattern2 = + /^https:\/\/github\.com\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+(\.git)?$/; + return (githubRepoPattern.test(link) || githubRepoPattern2.test(link)); +} + +export function isValidName(input: string): boolean { + // Regular expression to match only alphanumeric characters, hyphens, and underscores + const regex = /^[a-zA-Z0-9-_]+$/; + // Test if the input string matches the regular expression + return regex.test(input); +}