Skip to content

Commit

Permalink
feat: restructured whole add project form, new validation handler and…
Browse files Browse the repository at this point in the history
… more

Signed-off-by: Karthik Ayangar <[email protected]>
  • Loading branch information
kituuu committed May 19, 2024
1 parent b12ce77 commit 0f58c67
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 133 deletions.
57 changes: 32 additions & 25 deletions frontend/src/features/AddProject/index.scss
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand All @@ -43,33 +53,30 @@
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;
}

.add-project-btn img {
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 */

}
270 changes: 162 additions & 108 deletions frontend/src/features/AddProject/index.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(null);
const [description, setDescription] = useState<string | null>(null);
const [link, setLink] = useState<string | null>(null);

const [orgProject, setOrgProjects] = useState<Projects | null>(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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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: <b>Project saved</b>,
error: <b>Could not save</b>,
});
} 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: <b>Project saved</b>,
error: <b>Could not save</b>,
});
} 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 (
<div>
<div className='add-project-container'>
<form className='add-project-form'>
<div className='input-title'>Name</div>
<input
type='text'
placeholder='Project name'
onChange={nameChange}
value={name ? name : ''}
/>
{!name ? 'Name field should not be empty' : <></>}
{name && !isValidName(name) && 'Not a valid name'}
{name && !isUnique(name) && 'This project name already exists'}
<div className='input-title'>Project link</div>
<input
type='text'
value={link ? link : ''}
onChange={linkChange}
placeholder='Github link of project'
/>
{!link ? 'Link field should not be empty' : <></>}
{link &&
!isGitHubRepositoryLink(link) &&
'Not a valid github repository link'}
<div className='input-title'>Description</div>
<input
type='text'
value={description ? description : ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
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'}
</form>
<button className='add-project-btn' onClick={SubmitHandler}>
<div className='add-project-container'>
<form
onSubmit={handleSubmit}
className='add-project-form'
autoFocus={true}
autoComplete='off'
noValidate
>
<div className='input-title'>Name</div>
<input
type='text'
placeholder='Project name'
onChange={handleChange}
name='name'
value={form.name}
onBlur={handleBlur}
required
/>
{formErrors.name && <p className='form-error'>{formErrors.name}</p>}
<div className='input-title'>Project link</div>
<input
type='text'
value={form.link}
onChange={handleChange}
name='link'
placeholder='Github link of project'
required
onBlur={handleBlur}
/>
{formErrors.link && <p className='form-error'>{formErrors.link}</p>}
<div className='input-title'>
Description <span>(max char. 200)</span>
</div>
<input
type='text'
value={form.description}
onChange={handleChange}
name='description'
placeholder='Details about project'
onBlur={handleBlur}
maxLength={201}
/>
{formErrors.description && (
<p className='form-error'>{formErrors.description}</p>
)}
<button className='add-project-btnn' type='submit'>
<img src={tick} alt='' />
Done
Done
</button>
</div>
</form>
</div>
);
};
Expand Down
Loading

0 comments on commit 0f58c67

Please sign in to comment.