Skip to content
This repository has been archived by the owner on Apr 15, 2024. It is now read-only.

Commit

Permalink
Merge pull request #48 from HybridPlanner/fg+jvm/feat/autocomplete
Browse files Browse the repository at this point in the history
feat(autocomplete): add autocomplete
  • Loading branch information
Bricklou authored Dec 15, 2023
2 parents 8c8cc5a + 9e48aa5 commit 6d42501
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 169 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
"react-countdown": "^2.3.5",
"react-dom": "^18.2.0",
"react-hook-form": "^7.47.0",
"react-multi-email": "^1.0.18",
"react-router-dom": "^6.18.0",
"react-tag-autocomplete": "^7.1.0",
"sort-by": "^1.2.0",
"use-debounce": "^10.0.0",
"yup": "^1.3.2"
},
"devDependencies": {
Expand Down
7 changes: 7 additions & 0 deletions src/api/attendees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Attendee } from "@/types/Attendee";
import { apiClient } from ".";

export async function getAttendees(email: string): Promise<Attendee[]> {
const response = await apiClient.get<Attendee[]>(`/meetings/attendees?email=${email}`);
return response.data;
}
5 changes: 2 additions & 3 deletions src/api/meetings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

import { CreateMeetingInput, Meeting } from "@/types/Meeting";
import { CreateMeetingInput, CreateMeetingPayload, Meeting } from "@/types/Meeting";
import { apiClient } from ".";

/**
Expand Down Expand Up @@ -38,7 +37,7 @@ export async function getPreviousMeetings(before: Date): Promise<Meeting[]> {
* @returns The created Meeting object.
*/
export async function createMeeting(
data: CreateMeetingInput
data: CreateMeetingPayload
): Promise<Meeting> {
const response = await apiClient.post<Meeting>("/meetings", data);
return response.data;
Expand Down
3 changes: 1 addition & 2 deletions src/components/form/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ export type InputProps = {
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ id, label, error, className, ...props }, ref) => {
return (
<div className={classNames("relative mb-6 group", className)}>
{/* If the button has a label props, render it */}
<div className={classNames("relative group", className)}>
{label && (
<label
htmlFor={id}
Expand Down
18 changes: 0 additions & 18 deletions src/components/form/inputTags/Tag.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions src/components/form/inputTags/inputTag.css

This file was deleted.

81 changes: 28 additions & 53 deletions src/components/form/inputTags/inputTags.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import classNames from "classnames";
import { LucideIcon } from "lucide-react";
import { ReactElement, forwardRef } from "react";

import { ChangeHandler, Controller } from "react-hook-form";
import { ReactMultiEmail } from "react-multi-email";
import { TagElement } from "./Tag";
import "./inputTag.css";
import { ReactTags, Tag } from "react-tag-autocomplete";

export type InputTagsProps = {
id: string;
label?: string;
icon?: ReactElement<LucideIcon>;
error?: string;
suggestions?: string[];
suggestions?: Tag[];
placeholder: string;
className?: string;
disabled?: boolean;
value?: string[];
onChange?: (emails: string[]) => void;
value?: Tag[];
emptyState: string;
onChange?: (emails: Tag[]) => void;
onChangeInput?: (search: string) => void;
};

export const InputTags = forwardRef<HTMLDivElement, InputTagsProps>(
(
{ id, label, error, className, disabled, ...props }: InputTagsProps,
{
id,
label,
error,
className,
disabled,
suggestions,
emptyState,
...props
}: InputTagsProps,
ref
): JSX.Element => {
return (
Expand All @@ -36,51 +43,19 @@ export const InputTags = forwardRef<HTMLDivElement, InputTagsProps>(
{label}
</label>
)}
<div className="relative group">
{/* We're using the react-multi-email library to handle the mails input */}
<ReactMultiEmail
className={classNames(
"relative group inputTags",
"flex flex-wrap flex-row gap-2 items-center",

"transition-all duration-100 ease-in-out",
"py-2.5 px-3 w-full h-full outline-none border border-gray-300 rounded-lg shadow-xs",
"disabled:bg-gray-100 disable:text-gray-500",
{
"pl-10": props.icon,
},
error
? "border-red-500 focus:ring-2 ring-red-500 ring-offset-2"
: "group-focus-within:border-blue-500"
)}
inputClassName="outline-none"
placeholder={props.placeholder || label}
id={id}
aria-invalid={!!error ? "true" : "false"}
onChange={props.onChange}
emails={props.value}
getLabel={(email, index, removeEmail) => (
// To be able to customize the input, we're using our own TagElement component
<TagElement
key={index}
data-tag
onClick={() => removeEmail(index)}
email={email}
/>
)}
/>
{/* If the input tags has an icon props, render it */}
<div
className={classNames(
"absolute pointer-events-none inset-y-0 left-0 flex items-center pl-3 text-gray-500 peer-disabled:text-gray-500",
error ? "text-red-500" : "group-focus-within:text-blue-500"
)}
aria-hidden="true"
>
{props.icon}
</div>
</div>
{/* If the input tags has an error props, render it */}
<ReactTags
labelText={label}
selected={props.value ?? []}
suggestions={suggestions ?? []}
onAdd={(tag) => {
props.onChange?.([...(props.value ?? []), tag]);
}}
onDelete={(index) => {
props.onChange?.((props.value ?? []).filter((_, i) => i !== index));
}}
noOptionsText={emptyState}
/>
{error && (
<span className="text-sm text-red-500 px-2 absolute -bottom-6">
{error}
Expand Down
63 changes: 35 additions & 28 deletions src/components/meeting/MeetingDateBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,51 @@ export function MeetingDateBadge({
meeting,
className,
}: MeetingDateBadgeProps): ReactElement {
if (meeting.started) {
const formatDate = (start: Date, end: Date): string => {
if (
isToday(end) ||
isTomorrow(end) ||
format(start, "d") === format(end, "d")
) {
return format(end, "HH:mm");
} else {
return `${format(end, "EEEE d")} at ${format(end, "HH:mm")}`;
}
};
const formatOngoingDate = (start: Date, end: Date): string => {
if (
isToday(end) ||
isTomorrow(end) ||
format(start, "d") === format(end, "d")
) {
return format(end, "HH:mm");
} else {
return `${format(end, "EEEE d")} at ${format(end, "HH:mm")}`;
}
};

return (
<span
className={classNames(
className,
'before:content-[""] before:rounded-full before:w-2 before:h-2 before:bg-red-500 before:absolute',
"before:inline-block before:mr-2 before:top-1.5 before:left-2 before:animate-pulse",
const getContent = () => {
if (meeting.started) {
return (
"Ongoing until " +
formatOngoingDate(meeting.start_date, meeting.end_date)
);
}
return formatDate(meeting.start_date, meeting.end_date);
};

"bg-red-100 px-3 rounded-full text-red-700 relative font-medium pl-5"
)}
>
Ongoing until {formatDate(meeting.start_date, meeting.end_date)}
</span>
);
}
const getBadgeClass = () => {
if (meeting.started) {
return "before:bg-red-500 text-red-700 bg-red-100 before:animate-pulse";
}

if (meeting.end_date < new Date()) {
return "before:bg-gray-500 text-gray-700 bg-gray-100";
}

return "before:bg-blue-500 text-blue-700 bg-blue-100";
};

return (
<span
className={classNames(
className,
'before:content-[""] before:rounded-full before:w-2 before:h-2 before:bg-blue-500 before:relative',
'before:content-[""] before:rounded-full before:w-2 before:h-2 before:relative',
"before:inline-block before:mr-2 before:-top-0.5",
"bg-blue-100 px-3 py-1 rounded-full text-blue-700 relative font-medium"
"px-3 py-1 rounded-full relative font-medium w-fit",
getBadgeClass()
)}
>
{formatDate(meeting.start_date, meeting.end_date)}
{getContent()}
</span>
);
}
59 changes: 34 additions & 25 deletions src/components/meeting/MeetingInfoModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,17 @@ export const MeetingInfoModal = forwardRef<
)}
>
{meeting && (
<form method="dialog">
<form method="dialog" className="flex flex-col gap-2 justify-start">
{isBefore(new Date(), new Date(meeting.start_date)) && (
<button
id="edit"
aria-label="Edit meeting"
formNoValidate
className="absolute top-8 right-20 btn p-2 rounded-full hover:bg-gray-400 hover:bg-opacity-20"
>
<Pencil />
</button>
)}
<button
id="close"
aria-label="close"
Expand All @@ -42,32 +52,31 @@ export const MeetingInfoModal = forwardRef<
{meeting.title}
</h1>

<div className="m-4">
<MeetingDateBadge meeting={meeting} className="mb-4 inline-block" />
<MeetingDateBadge meeting={meeting} className="mb-4 inline-block" />

<div className="flex flex-col-reverse md:flex-row">
<div className="flex-1">
<p>{meeting.description}</p>
{meeting.description && (
<>
<h4 className="font-semibold">Description</h4>
<p>{meeting.description}</p>
</>
)}

<div className="my-4">
<h2 className="font-semibold mb-2">Attendees</h2>
{/* Attendees list */}
<ul className="flex flex-wrap">
{meeting.attendees.map((attendee) => (
<li
key={attendee.id}
className={classNames(
"bg-purple-300 text-purple-800 bg-opacity-50 rounded-full px-4",
"before:rounded-full"
)}
>
{attendee.email}
</li>
))}
</ul>
</div>
</div>
</div>
<div className="my-4">
<h4 className="font-semibold mb-2">Attendees</h4>
<ul className="flex gap-2 flex-wrap">
{meeting.attendees.map((attendee) => (
<li
key={attendee.id}
className={classNames(
"px-3 py-1 rounded-full relative font-medium bg-purple-100 text-purple-700"
)}
>
{attendee.email}
</li>
))}
</ul>

<aside className="w-full md:w-52">{/* Meeting actions */}</aside>
</div>
</form>
)}
Expand Down
Loading

0 comments on commit 6d42501

Please sign in to comment.