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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions components/reusable-components/add-skill/AddSkill.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.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;
background-color: transparent;
border: none;
}
}
}
58 changes: 58 additions & 0 deletions components/reusable-components/add-skill/AddSkills.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'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}
isOpen
/>
)}
<div className={styles.selectedTags}>
{selectedTags.map((tag) => (
<div key={tag} className={styles.selectedTag}>
<button
aria-label="Remove"
className={styles.removeTag}
type="button"
onClick={() => setSelectedTags(selectedTags.filter((item) => item !== tag))}
>
<i className="bi bi-x" />
</button>{' '}
{tag}
</div>
))}
</div>
</div>
);
};

export default AddSkill;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import Button from '../button/Button';
interface ReusableModalProps {
title: string;
description?: string;
// eslint-disable-next-line no-undef
children?: JSX.Element;
isOpen: boolean;
onClose?: () => void;
secondaryButtonLabel?: string;
Expand All @@ -18,6 +20,7 @@ interface ReusableModalProps {
const ReusableModal = ({
title,
description,
children,
isOpen,
secondaryButtonLabel = 'Cancel',
secondaryButtonClass = 'secondaryButton',
Expand Down Expand Up @@ -80,6 +83,7 @@ const ReusableModal = ({
<div className={style.modalContent}>
<h2>{title}</h2>
{description && <p>{description}</p>}
{children}
</div>
<div className={style.modalButtons}>
<Button
Expand Down
56 changes: 56 additions & 0 deletions components/reusable-components/tags-modal/TagsModal.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.modalContent {
width: 100%;
position: relative;
}

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

.dropdown {
position: absolute;
width: 100%;
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;
}
}
}
118 changes: 118 additions & 0 deletions components/reusable-components/tags-modal/TagsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */

'use client';

import React, { ChangeEvent, useEffect, useState } from 'react';
import styles from './TagsModal.module.scss';
import ReusableModal from '../reusable-modal/ReusableModal';

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

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

useEffect(() => {
if (isOpen) {
setSelectedSkills(selectedTags);
}
}, [isOpen, selectedTags]);

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 (!selectedSkills.includes(tag)) {
setSelectedSkills([...selectedSkills, tag]);
}
setSearchTag('');
setFilteredTags([]);
};

const removeTag = (tag: string) => {
setSelectedSkills(selectedSkills.filter((item) => item !== tag));
};

const handleSave = () => {
setSelectedTags(selectedSkills);
onClose();
};

const handleCancel = () => {
onClose();
};

return (
<ReusableModal
title="Add Skill"
isOpen={isOpen}
onClose={handleCancel}
onPrimaryButtonClick={handleSave}
onSecondaryButtonClick={handleCancel}
>
<div className={styles.modalContent}>
<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}>
{selectedSkills.map((skill) => (
<span key={skill} className={styles.tag}>
<span className={styles.removeTag} onClick={() => removeTag(skill)}>
<i className="bi bi-x" />
</span>
{skill}
</span>
))}
</div>
</div>
</ReusableModal>
);
};

export default TagsModal;
Loading
Loading