Skip to content

Commit

Permalink
added autocomplete dropdown and new endpoint to search people in the …
Browse files Browse the repository at this point in the history
…organization
  • Loading branch information
ali-ahnaf committed Jan 11, 2025
1 parent 98b13fc commit 17272a8
Show file tree
Hide file tree
Showing 13 changed files with 294 additions and 141 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,12 @@ During peak times, manually searching for a free room on Google Calendar is frus
3. Set up your environment. [Calender Quickstart](https://developers.google.com/calendar/api/quickstart/js#set_up_your_environment)
4. Add the following scopes to the new credential (OAuth web client id): `./auth/userinfo.profile` and `./auth/calendar`
5. Enable the [Admin SDK API](https://console.cloud.google.com/apis/api/admin.googleapis.com/overview). This is required to read the directory resources; refer to `getCalendarResources()`
6. Copy the **Client ID**, **Client secret** from the OAuth 2.0 Client ID you created in step 1, and place it in the `.env` file.
7. Add the **Authorized javascript origins** and the **Authorized redirect URIs** in your google cloud project.
8. Check out the [installation](CONTRIBUTING.md/#installation-with-docker-1) section, to prepare the app for launching.
9. Deploy your branch and your ready to start booking! 🎉
10. Make sure to sync latest changes from the upstream repository.
6. Enable the [People API](https://console.cloud.google.com/apis/api/people.googleapis.com). This is requred to obtain the emails of the people in your organization, used when searching attendees.
7. Copy the **Client ID**, **Client secret** from the OAuth 2.0 Client ID you created in step 1, and place it in the `.env` file.
8. Add the **Authorized javascript origins** and the **Authorized redirect URIs** in your google cloud project.
9. Check out the [installation](CONTRIBUTING.md/#installation-with-docker-1) section, to prepare the app for launching.
10. Deploy your branch and your ready to start booking! 🎉
11. Make sure to sync latest changes from the upstream repository.

**Note**: The **Authorized javascript origins** should have:
```bash
Expand Down
2 changes: 1 addition & 1 deletion client/generate-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const manifest = {
icons: {
16: 'favicon-16x16.png',
32: 'favicon-32x32.png',
128: 'logo128.png',
128: 'logo_128.png',
},
permissions: ['identity', 'storage'],
};
Expand Down
8 changes: 4 additions & 4 deletions client/public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{
"name": "QuickMeet",
"name": "Quick Meet",
"version": "1.0.2",
"description": "Book meeting rooms with a single click",
"description": "Book rooms with a single click",
"manifest_version": 3,
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAstDgQFGfJrfxo3id/1KTHVZpKbsCRMKZJXDUsDT8JRfasB/CeDGmuVs1hFYBJcGgn9PbK/mnE9hzWERVFpa4sfGZ3o0lvyfPLUfJd7PmfZ/4PQvE4+GfVQPAz/p4OVtP1WtbN4DED3jmrXiSrZ72paioz/ydVOSRDfo8m3+s9K92LcraYXHItvs+rSKXgfKxflfGzByme/fVO2V4yvE6T0YOqdLDc2USF4KGx0llHvqnmtB2K+rLr3V1/UcM1b4fP6kCiZAo7K2Tngpqa8DxgLVp8GYZ7NPPFJqu4tG1G1iRjtwk8Qblqmw+jmH+qZ2WguGtFpxU7P2JD8znPu//OwIDAQAB",
"author": "Ali Ahnaf",
"action": {
"default_popup": "index.html",
"default_icon": "favicon-32x32.png",
"default_title": "QuickMeet"
"default_title": "Quick Meet"
},
"host_permissions": [
"http://localhost:3000/*"
],
"icons": {
"16": "favicon-16x16.png",
"32": "favicon-32x32.png",
"128": "logo128.png"
"128": "logo_128.png"
},
"permissions": [
"identity",
Expand Down
13 changes: 13 additions & 0 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ export default class Api {
}
}

async searchPeople(email: string): Promise<ApiResponse<string[]>> {
try {
const res = await this.client.get('/api/directory/people', {
params: {
email,
},
});

return res.data as ApiResponse<string[]>;
} catch (error: any) {
return this.handleError(error);
}
}
async getFloors() {
try {
const res = await this.client.get('/api/floors');
Expand Down
207 changes: 207 additions & 0 deletions client/src/components/AttendeeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { TextField, Chip, Box, Autocomplete, debounce, IconButton, styled, Popper } from '@mui/material';
import PeopleAltRoundedIcon from '@mui/icons-material/PeopleAltRounded';
import { useState } from 'react';
import { useApi } from '@/context/ApiContext';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import { isEmailValid } from '@/helpers/utility';
import toast from 'react-hot-toast';

interface AttendeeInputProps {
id: string;
value?: any[];
disabled?: boolean;
onChange: (id: string, value: string[]) => void;
type?: string;
}

const StyledPopper = styled(Popper)(({ theme }) => ({
'& .MuiAutocomplete-paper': {
backgroundColor: 'rgba(245, 245, 245)',
color: theme.palette.text.primary,
boxShadow: '0px 4px 10px rgba(0, 0, 0, 0.1)',
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(0.5),
},
'& .MuiAutocomplete-listbox': {
padding: 0,
display: 'flex',
flexDirection: 'column',
},
'& .MuiAutocomplete-option': {
padding: theme.spacing(1.5),
borderRadius: theme.shape.borderRadius,
},
}));

export default function AttendeeInput({ id, onChange, value, type }: AttendeeInputProps) {
const [options, setOptions] = useState<string[]>([]);
const [textInput, setTextInput] = useState('');

const api = useApi();

const handleInputChange = async (_: React.SyntheticEvent, newInputValue: string) => {
if (newInputValue.length > 2) {
const res = await api.searchPeople(newInputValue);
if (res.status === 'success') {
setOptions(res.data || []);
}
}
};

const handleSelectionChange = (_: React.SyntheticEvent, newValue: string[]) => {
if (newValue.length > 0) {
const lastValue = newValue[newValue.length - 1].trim();
if (isEmailValid(lastValue)) {
onChange(id, newValue);
setTextInput('');
} else {
toast.error('Invalid email entered');
}
} else {
onChange(id, newValue);
setTextInput('');
}
};

const handleKeyDown = (event: any) => {
if (event.key === ' ') {
event.preventDefault();
const inputValue = event.target.value.trim();
const existingEmails = value || [];

if (existingEmails.find((email) => email === inputValue)) {
toast.error('Duplicate email entered');
return;
}

if (!isEmailValid(inputValue)) {
toast.error('Invalid email entered');
return;
}

onChange(id, [...existingEmails, inputValue]);
setTextInput('');
}
};

const onRemoveAllClick = () => {
onChange(id, []);
};

const debouncedInputChange = debounce(handleInputChange, 300);

return (
<Box
display="flex"
alignItems="center"
flexWrap="wrap"
sx={[
(theme) => ({
gap: '8px',
padding: '10px',
borderRadius: 1,
backgroundColor: theme.palette.common.white,
'&:focus-within': {
border: 'none',
},
maxHeight: '65px',
overflowY: 'auto',
}),
]}
>
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
mx: 1,
}}
>
<PeopleAltRoundedIcon
sx={[
(theme) => ({
color: theme.palette.grey[50],
position: 'sticky',
top: 0,
bottom: 0,
}),
]}
/>
<Autocomplete
multiple
options={options}
value={value || []}
getOptionLabel={(option) => option}
freeSolo
inputValue={textInput}
fullWidth
onChange={handleSelectionChange}
onInputChange={debouncedInputChange}
renderTags={(value: readonly string[], getTagProps) =>
value.map((option: string, index: number) => {
const { key, ...tagProps } = getTagProps({ index });
return <Chip variant="filled" label={option} key={key} {...tagProps} />;
})
}
renderInput={(params) => (
<TextField
{...params}
onChange={(e) => setTextInput(e.target.value)}
type={type}
variant="standard"
placeholder="Attendees"
onKeyDown={handleKeyDown}
slotProps={{
input: {
...params.InputProps,
endAdornment: null,
},
}}
sx={[
(theme) => ({
flex: 1,
py: 0,
px: 1,
'& .MuiInputBase-input': {
fontSize: theme.typography.subtitle1,
},
'& .MuiInputBase-input::placeholder': {
color: theme.palette.primary.main,
fontSize: theme.typography.subtitle1,
},
'& .MuiInput-underline:before, & .MuiInput-underline:hover:before': {
borderBottom: 'none !important',
},
'& .MuiInput-underline:after': {
borderBottom: 'none',
},
}),
]}
/>
)}
PopperComponent={StyledPopper}
/>
<IconButton
sx={{
position: 'sticky',
p: 0.5,
top: 0,
bottom: 0,
display: (value || []).length > 0 ? 'flex' : 'none',
}}
onClick={onRemoveAllClick}
>
<CloseRoundedIcon
fontSize="small"
sx={[
(theme) => ({
color: theme.palette.grey[50],
}),
]}
/>
</IconButton>
</Box>
</Box>
);
}
115 changes: 0 additions & 115 deletions client/src/components/ChipInput.tsx

This file was deleted.

Loading

0 comments on commit 17272a8

Please sign in to comment.