Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#386 add tags component #426

Merged
merged 9 commits into from
Aug 18, 2024
37 changes: 37 additions & 0 deletions components/reusable-components/add-skill/AddSkill.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.addSkillWrapper {
position: relative;
}

.addButton {
padding: 8px 16px;
background: #212f42;
color: #aad6ff;
border: 3px solid #aad6ff;
border-radius: 20px;
cursor: pointer;
font-weight: 700;
font-size: 18px;
margin-top: 20px;
}

.selectedTags {
display: flex;
flex-wrap: wrap;
margin-top: 16px;

.selectedTag {
background: white;
color: #212f42;
border: 1px solid #212f42;
padding: 4px 8px;
border-radius: 10px;
margin: 4px;
display: flex;
align-items: center;
gap: 3px;

.removeTag {
cursor: pointer;
}
}
}
57 changes: 57 additions & 0 deletions components/reusable-components/add-skill/AddSkills.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

import { useState } from 'react';
import styles from './AddSkill.module.scss';
import TagsModal from '../tags-modal/TagsModal';

const existingTags = [
'JavaScript',
'React',
'Next.js',
'CSS',
'HTML',
'Node.js',
'Java',
'C#',
'PHP',
];

const AddSkill = () => {
const [showModal, setShowModal] = useState(false);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const openModal = () => setShowModal(true);
const closeModal = () => setShowModal(false);

return (
<div className={styles.addSkillWrapper}>
<button type="button" onClick={openModal} className={styles.addButton}>
+ Add Skill
</button>
{showModal && (
<TagsModal
existingTags={existingTags}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
onClose={closeModal}
/>
)}
<div className={styles.selectedTags}>
{/* eslint-disable jsx-a11y/no-static-element-interactions */}
{/* eslint-disable jsx-a11y/click-events-have-key-events */}
Jasminamih marked this conversation as resolved.
Show resolved Hide resolved
{selectedTags.map((tag) => (
<div key={tag} className={styles.selectedTag}>
<span
className={styles.removeTag}
onClick={() => setSelectedTags(selectedTags.filter((item) => item !== tag))}
>
<i className="bi bi-x" />
</span>{' '}
{tag}
</div>
))}
</div>
</div>
);
};

export default AddSkill;
87 changes: 87 additions & 0 deletions components/reusable-components/tags-modal/TagsModal.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
.modalWrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #121d361f;
display: flex;
justify-content: center;
align-items: center;
}

.modalContent {
background: white;
padding: 30px;
border-radius: 8px;
position: relative;
width: 300px;
}

.closeButton {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 20px;
cursor: pointer;
}

.searchInput {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.skillLabel {
color: #121d36a5;
}

.dropdown {
position: absolute;
top: 80px;
left: 50%;
transform: translate(-50%);
width: 80%;
max-height: 200px;
overflow-y: auto;
background: white;
color: #121d36;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;

.tagItem {
padding: 8px;
cursor: pointer;

&:hover {
background: #121d36cb;
color: rgb(255, 255, 255);
}
}
}

.selectedTags {
display: flex;
flex-wrap: wrap;
margin-top: 8px;

.tag {
background: transparent;
color: #121d36;
padding: 4px 8px;
border: 1px solid #121d36;
border-radius: 10px;
margin: 4px;
display: flex;
align-items: center;
gap: 3px;

.removeTag {
cursor: pointer;
}
}
}
95 changes: 95 additions & 0 deletions components/reusable-components/tags-modal/TagsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */

'use client';

import React, { ChangeEvent, useState } from 'react';
import styles from './TagsModal.module.scss';

interface TagSearchProps {
existingTags: string[];
selectedTags: string[];
setSelectedTags: (items: string[]) => void;
onClose: () => void;
}

const TagsModal = ({ existingTags, selectedTags, setSelectedTags, onClose }: TagSearchProps) => {
const [searchTag, setSearchTag] = useState<string>('');
const [filteredTags, setFilteredTags] = useState<string[]>([]);

const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
const query = e.target.value;
setSearchTag(query);

if (query) {
const filtered = existingTags.filter((tag) =>
tag.toLowerCase().includes(query.toLowerCase())
);
setFilteredTags(filtered);
} else {
setFilteredTags([]);
}
};
edichoska marked this conversation as resolved.
Show resolved Hide resolved

const addTag = (tag: string) => {
if (!selectedTags.includes(tag)) {
setSelectedTags([...selectedTags, tag]);
}
setSearchTag('');
setFilteredTags([]);
};

const removeTag = (tag: string) => {
setSelectedTags(selectedTags.filter((item) => item !== tag));
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid issues with stale state, we should use the snapshot of the most recent state. something like: setSelectedTags((prevTags) ....


return (
<div className={styles.modalWrapper}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the reusable modal here

<div className={styles.modalContent}>
<button type="button" className={styles.closeButton} onClick={onClose}>
<i aria-label="Close button" className="bi bi-x" />
</button>
{/* eslint-disable jsx-a11y/label-has-associated-control */}
Jasminamih marked this conversation as resolved.
Show resolved Hide resolved
<label className={styles.skillLabel} htmlFor="searchTagInput">
Skill
</label>
<input
id="searchTagInput"
type="text"
value={searchTag}
onChange={handleSearchChange}
placeholder="Search or add a tag"
className={styles.searchInput}
name="searchTagInput"
/>
{searchTag && (
<div className={styles.dropdown}>
{filteredTags.length > 0 ? (
filteredTags.map((tag) => (
<div key={tag} onClick={() => addTag(tag)} className={styles.tagItem}>
{tag}
</div>
))
) : (
<div onClick={() => addTag(searchTag)} className={styles.tagItem}>
Add {searchTag}
</div>
)}
</div>
)}
<div className={styles.selectedTags}>
{selectedTags.map((tag) => (
<span key={tag} className={styles.tag}>
<span className={styles.removeTag} onClick={() => removeTag(tag)}>
<i className="bi bi-x" />
</span>{' '}
{tag}
</span>
))}
</div>
</div>
</div>
);
};

export default TagsModal;