create new session page for beta site

This commit is contained in:
Nuwan 2023-02-12 12:06:16 +05:30
parent b589ad8553
commit fa082230b8
18 changed files with 443 additions and 231 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,5 +6,6 @@
@import './custom/nav';
@import './custom/user';
@import './custom/form';
@import './custom/form';
@import './custom/chips';
@import './custom/common';

View File

@ -0,0 +1,88 @@
.chip-container{
border: 1px solid #edf2f9f5;
width: 100%;
padding: 10px;
background: floralwhite;
//font-family: "Roboto";
.basic-chip a{
color: inherit;
}
.chip-title{
padding: 20px 10px;
font-size: 14px;
}
.basic-chip{
padding: 5px 10px;
border-radius: 50px;
display: inline-flex;
margin: 5px;
}
.click-chip, .click-chip-hover{
padding: 5px 10px;
border-radius: 50px;
display: inline-flex;
margin: 5px;
cursor: pointer;
}
.click-chip-hover:hover{
filter: brightness(85%);
}
.outline{
border: 1.5px solid #cccccc;
color: #cccccc;
outline: none;
}
.outline-green{
border: 1.5px solid #3a913f;
color: #3a913f;
outline: none;
}
.outline-blue{
border: 1.5px solid #0074c3;
color: #0074c3;
outline: none;
}
.background-grey{
background: #dddddd;
color: #616161;
}
.background-white{
border: 1.5px solid #d8e2ef;
background: #ffffff;
color: #d0d0d0;
border-radius: 10px;
}
.background-green{
background: #3a913f;
color: #ffffff;
}
.background-blue{
background: #0074c3;
color: #ffffff;
}
.icon{
/*background: #bdbdbd;*/
color: #616161;
margin: 0px 5px 0px -5px;
background: #bbbbbb;
padding: 2px;
border-radius: 50px;
}
.fa-times, .fa-times-circle{
margin: 0px -5px 0px 5px;
}
}

View File

@ -1,126 +1,149 @@
import React, { useState, useEffect } from 'react';
import { Form, FormGroup, Input, Label, Text, Card, CardBody, Button } from 'reactstrap';
import React, { useRef, useState, useEffect } from 'react';
import { Form, FormGroup, Input, Label, Card, CardBody, Button, Row, Col } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import Flex from '../common/Flex';
import JKTooltip from '../common/JKTooltip';
import { useTranslation } from 'react-i18next';
import AsyncSelect from 'react-select/async';
//import useAutoComplete from '../../hooks/useAutocomplete';
import { useAuth } from '../../context/UserAuth';
import JKFriendsAutoComplete from '../people/JKFriendsAutoComplete';
import JKSessionInviteesChips from '../people/JKSessionInviteesChips';
import { getFriends } from '../../helpers/rest';
import Avatar from '../common/Avatar';
const CustomOption = props => {
const { data, innerRef, innerProps } = props;
return (
<div ref={innerRef} {...innerProps}>
<Flex direction="row" className="p-2 bg-200 mb-2">
<div className="p-2 bg-300">
<Avatar src={data.photoUrl} size="s" name={data.label} />
</div>
<div className="p-2 bg-300">{data.label}</div>
</Flex>
</div>
);
};
const JKNewMusicSession = () => {
const { t } = useTranslation();
const { currentUser } = useAuth();
const { t } = useTranslation();
const [friends, setFriends] = useState([]);
const [isFriendsFetched, setIsFriendsFetched] = useState(false)
const [description, setDescription] = useState("")
const [invitees, setInvitees] = useState([]);
const [privacy, setPrivacy] = useState("1");
const friendOptions = async inputValue => {
let matches = [];
if (inputValue && inputValue.length >= 3) {
await getFriends(currentUser.id)
.then(resp => {
return resp.json();
})
.then(data => {
matches = data
.filter(
friend =>
friend.first_name.toLowerCase().includes(inputValue.toLowerCase()) ||
friend.last_name.toLowerCase().includes(inputValue.toLowerCase())
)
.map(opt => ({
photoUrl: opt.photo_url,
label: `${opt.first_name} ${opt.last_name}`,
value: opt.id
}));
});
useEffect(() => {
fetchFriends();
}, []);
useEffect(() => {
if(isFriendsFetched){
populateFormDataFromLocalStorage()
}
return matches;
}, [isFriendsFetched])
const fetchFriends = () => {
getFriends(currentUser.id)
.then(resp => {
if (resp.ok) {
return resp.json();
}
})
.then(data => {
setFriends(data);
setIsFriendsFetched(true);
});
}
const populateFormDataFromLocalStorage = () => {
try {
const formData = localStorage.getItem('formData');
const formDataItems = JSON.parse(formData);
setDescription(formDataItems['description']);
setPrivacy(formDataItems['privacy']);
const inviteeIds = formDataItems['inviteeIds'];
const invitees = friends.filter(f => inviteeIds.includes(f.id));
updateSessionInvitations(invitees);
} catch (error) {
console.error('localStorage is not available', error)
}
}
const handleSubmit = event => {
event.preventDefault();
const formData = new FormData(event.target);
const payload = {
privacy: formData.get('privacy'),
description: formData.get('description'),
inviteeIds: invitees.map(i => i.id)
};
console.log(payload); //TODO: handle payload
try {
//store this payload in localstorage.
localStorage.setItem('formData', JSON.stringify(payload))
} catch (error) {
console.error("localStorage is not available", error);
}
};
const handleSubmit = (event) => {
event.preventDefault()
const formData = new FormData(event.target)
console.log(formData.get('privacy'));
console.log(formData.get('friendIds'));
console.log(formData.get('description'));
const handleOnSelect = submittedItems => {
updateSessionInvitations(submittedItems)
};
const updateSessionInvitations = (submittedInvitees) => {
const updatedInvitees = Array.from(new Set([...invitees, ...submittedInvitees]));
setInvitees(updatedInvitees);
const friendIds = submittedInvitees.map(si => si.id)
const updatedFriends = friends.filter(f => !friendIds.includes(f.id));
setFriends(updatedFriends);
}
const removeInvitee = invitee => {
const updatedInvitees = invitees.filter(i => i.id !== invitee.id);
setInvitees(updatedInvitees);
const updatedFriends = [...friends, invitee];
setFriends(updatedFriends);
};
return (
<div>
<Card>
<FalconCardHeader title={t('page_title', { ns: 'people' })} titleClass="font-weight-bold" />
<FalconCardHeader title={t('new.page_title', { ns: 'sessions' })} titleClass="font-weight-bold" />
<CardBody className="pt-0">
<Form onSubmit={handleSubmit}>
<FormGroup className="mb-3">
<Label>
Session Privacy{' '}
<JKTooltip title="Public sessions can be seen in the Browse Sessions feature by other musicians in the JamKazam community, and others can join your session. If you choose the “Private - only invited musicians can join” option, your session will not be visible to the community as a whole, and only those musicians you invite will be able to see or join your session. If you choose the “Private anyone can request to join, but requires approval” option, your session will be visible to the community in the Browse Sessions feature, and non-invited musicians may request to join your session, but you will have to grant permission per user to allow users into your session." />
</Label>
<Input type="select" aria-label="Session Privacy" name="privacy">
<option value="1">Public anyone can join</option>
<option value="2">Private only invited musicians can join</option>
<option value="3">Private anyone can request to join, but requires approval</option>
</Input>
</FormGroup>
<Row>
<Col>
<Form onSubmit={handleSubmit}>
<FormGroup className="mb-3">
<Label>
{t('new.privacy', { ns: 'sessions' })}{' '}
<JKTooltip title={t('new.privacy_help', { ns: 'sessions' })} />
</Label>
<Input type="select" aria-label="Session Privacy" name="privacy" value={privacy} onChange={(e) => setPrivacy(e.target.value)}>
<option value="1">{t('new.privacy_opt_public', { ns: 'sessions' })}</option>
<option value="2">{t('new.privacy_opt_private_invite', { ns: 'sessions' })}</option>
<option value="3">{t('new.privacy_opt_private_approve', { ns: 'sessions' })}</option>
</Input>
</FormGroup>
<FormGroup className="mb-3">
<Label>
Session Invitations{' '}
<JKTooltip title="If you invite other users to join your session, this will generate an in-app notification and in some cases also an email invitation to invitees. Invited users will also see your session in the “For Me” section of the Browse Sessions feature. Invited users can join your session without further permission or action on your part, regardless of whether your session is public or private." />
</Label>
<FormGroup className="mb-3">
<Label>
{t('new.invitations', { ns: 'sessions' })}{' '}
<JKTooltip title={t('new.invitations_help', { ns: 'sessions' })} />
</Label>
<JKFriendsAutoComplete friends={friends} onSelect={handleOnSelect} />
<JKSessionInviteesChips invitees={invitees} removeInvitee={removeInvitee} />
</FormGroup>
<AsyncSelect
name="friendIds"
placeholder="Enter friend name"
isMulti
loadOptions={friendOptions}
noOptionsMessage={e => (e.inputValue ? 'No options' : null)}
components={{
Option: CustomOption,
DropdownIndicator: () => null,
IndicatorSeparator: () => null
}}
styles={{
control: (baseStyles, state) => ({
...baseStyles,
borderRadius: '1.25em'
})
}}
/>
</FormGroup>
<FormGroup className="mb-3">
<Label>
{t('new.description', { ns: 'sessions' })}{' '}
<JKTooltip title={t('new.description_help', { ns: 'sessions' })} />
</Label>
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
name="description"
type="textarea"
placeholder={t('new.description_placeholder', { ns: 'sessions' })}
/>
</FormGroup>
<FormGroup className="mb-3">
<Label>
Session Description{' '}
<JKTooltip title="If youre creating a public session, we strongly recommend that you enter a description of your session for example, what kinds of music youre interested in playing. This description will be displayed next to your session in the Browse Sessions feature, which will help other musicians in the community understand if your session is a good fit for them." />
</Label>
<Input
name="description"
type="textarea"
placeholder="Enter session description. Recommended for public sessions to attract other musicians and them know what
to expect in your session."
/>
</FormGroup>
<FormGroup className="mb-3">
<Button color="primary">Create Session</Button>
</FormGroup>
</Form>
<FormGroup className="mb-3">
<Button color="primary">{t('new.create_session', { ns: 'sessions' })}</Button>
</FormGroup>
</Form>
</Col>
<Col />
</Row>
</CardBody>
</Card>
</div>

View File

@ -0,0 +1,103 @@
import React, { useState, useRef } from 'react';
import { Input, InputGroup, InputGroupText, ListGroup, ListGroupItem, Row, Col, Button } from 'reactstrap';
import JKSelectFriendsModal from './JKSelectFriendsModal';
import { useTranslation } from 'react-i18next';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
function JKFriendsAutoComplete({ friends, onSelect }) {
const [filteredFriends, setFilteredFriends] = useState([]);
const [showDropdown, setShowDropdown] = useState(false);
const [inputValue, setInputValue] = useState('');
const [showModal, setShowModal] = useState(false);
const inputRef = useRef(null);
const { t } = useTranslation();
const MIN_FILTER_SIZE = 3;
const handleInputChange = e => {
const val = e.target.value;
setInputValue(val);
if (val && val.length >= MIN_FILTER_SIZE) {
const filtered = friends.filter(
friend =>
friend.first_name.toLowerCase().includes(val.toLowerCase()) ||
friend.last_name.toLowerCase().includes(val.toLowerCase())
);
setFilteredFriends(filtered);
setShowDropdown(true);
} else {
setShowDropdown(false);
}
};
const handleOnClick = friend => {
onSelect([friend]);
handleAfterSelect();
};
const handleAfterSelect = () => {
setShowDropdown(false);
inputRef.current.focus();
setInputValue('');
};
const handleOnKeyDown = event => {
if (event.key !== undefined) {
if (event.key === 'Enter') {
const first = filteredFriends[0];
onSelect([first]);
handleAfterSelect();
} else if (event.key === 'ArrowDown') {
console.log(event.target);
}
}
};
const highlight = event => {
event.target.classList.add('bg-light');
};
const unhighlight = event => {
event.target.classList.remove('bg-light');
};
const onSubmitFriendsModal = selectedFriends => {
onSelect(selectedFriends);
};
const toggleVisibility = (isVisible) => {
setShowModal(isVisible)
}
return (
<div>
<Row className="mb-2">
<Col md={9}>
<InputGroup>
<InputGroupText>
<FontAwesomeIcon icon="search" transform="shrink-4 down-1" />
</InputGroupText>
<Input onChange={handleInputChange} onKeyDown={handleOnKeyDown} value={inputValue} innerRef={inputRef} placeholder={t('new.invitations_filter_placeholder', { ns: 'sessions' })} />
</InputGroup>
</Col>
<Col md={3}>
<Button variant="outline-info" outline onClick={() => setShowModal(!showModal)}>
{t('new.choose_friends', { ns: 'sessions' })}
</Button>
</Col>
</Row>
<ListGroup className={showDropdown ? 'd-block' : 'd-none'}>
{filteredFriends.map(f => (
<ListGroupItem key={f.id} onMouseOver={highlight} onMouseOut={unhighlight} onClick={() => handleOnClick(f)}>
{f.first_name} {f.last_name}
</ListGroupItem>
))}
</ListGroup>
<JKSelectFriendsModal friends={friends} show={showModal} toggleVisibility={toggleVisibility} onSubmit={onSubmitFriendsModal} />
</div>
);
}
export default JKFriendsAutoComplete;

View File

@ -0,0 +1,63 @@
import React, { useState, useEffect } from 'react';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, ListGroup, ListGroupItem, Input } from 'reactstrap';
import { useTranslation } from 'react-i18next';
function JKSelectFriendsModal({ show, friends, onSubmit, toggleVisibility }) {
const [modal, setModal] = useState(false);
const [selected, setSelected] = useState([])
const { t } = useTranslation();
const toggle = () => {
toggleVisibility(!modal)
setModal(!modal);
}
useEffect(() => {
setModal(show);
}, [show]);
const onCheckBoxClick = (event) => {
const friend = friends.find(f => f.id === event.target.value)
if(event.target.checked){
const updated = Array.from(new Set([...selected, friend]))
setSelected(updated)
}else{
const updated = selected.filter(s => s.id !== friend.id)
setSelected(updated)
}
}
const onButtonClick = () => {
onSubmit(selected)
setSelected([])
toggle()
}
return (
<div>
<Modal isOpen={modal} toggle={toggle} scrollable={true}>
<ModalHeader toggle={toggle}>Invite Friends to Session</ModalHeader>
<ModalBody>
<ListGroup flush>
{friends.map(f => (
<ListGroupItem key={f.id}>
<Input type="checkbox" onClick={onCheckBoxClick} value={f.id} />
{f.first_name} {f.last_name}
</ListGroupItem>
))}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="secondary" outline onClick={toggle}>
{t('new.cancel', { ns: 'sessions' })}
</Button>{' '}
<Button color="primary" onClick={onButtonClick}>
{t('new.add_friends', { ns: 'sessions' })}
</Button>
</ModalFooter>
</Modal>
</div>
);
}
export default JKSelectFriendsModal;

View File

@ -0,0 +1,25 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons'
function JKSessionInviteesChips({ invitees, removeInvitee }) {
const remove = (event, invitee) => {
event.preventDefault();
removeInvitee(invitee);
}
return (
<div className="chip-container">
{invitees &&
invitees.map(i => (
<div key={i.id} className="basic-chip background-white">
{ i.first_name} {i.last_name }
<a onClick={(e) => remove(e, i)}>
<FontAwesomeIcon icon={faTimesCircle} />
</a>
</div>
))}
</div>
);
}
export default JKSessionInviteesChips;

View File

@ -1,118 +0,0 @@
import React, { useRef, useState } from 'react';
const KEY_CODES = {
DOWN: 40,
UP: 38,
PAGE_DOWN: 34,
ESCAPE: 27,
PAGE_UP: 33,
ENTER: 13
};
export default function useAutoComplete({ delay = 500, source, onChange }) {
const [myTimeout, setMyTimeOut] = useState(setTimeout(() => {}, 0));
const listRef = useRef();
const [suggestions, setSuggestions] = useState([]);
const [isBusy, setBusy] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [textValue, setTextValue] = useState('');
function delayInvoke(cb) {
if (myTimeout) {
clearTimeout(myTimeout);
}
setMyTimeOut(setTimeout(cb, delay));
}
function selectOption(index) {
if (index > -1) {
onChange(suggestions[index]);
setTextValue(suggestions[index].label);
}
clearSuggestions();
}
async function getSuggestions(searchTerm) {
if (searchTerm && source) {
const options = await source(searchTerm);
setSuggestions(options);
}
}
function clearSuggestions() {
setSuggestions([]);
setSelectedIndex(-1);
}
function onTextChange(searchTerm) {
setBusy(true);
setTextValue(searchTerm);
clearSuggestions();
delayInvoke(() => {
getSuggestions(searchTerm);
setBusy(false);
});
}
const optionHeight = listRef?.current?.children[0]?.clientHeight;
function scrollUp() {
if (selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
}
//listRef?.current?.scrollTop -= optionHeight
}
function scrollDown() {
if (selectedIndex < suggestions.length - 1) {
setSelectedIndex(selectedIndex + 1);
}
//listRef?.current?.scrollTop = selectedIndex * optionHeight
}
function pageDown() {
setSelectedIndex(suggestions.length - 1);
//listRef?.current?.scrollTop = suggestions.length * optionHeight
}
function pageUp() {
setSelectedIndex(0);
//listRef?.current?.scrollTop = 0
}
function onKeyDown(e) {
const keyOperation = {
[KEY_CODES.DOWN]: scrollDown,
[KEY_CODES.UP]: scrollUp,
[KEY_CODES.ENTER]: () => selectOption(selectedIndex),
[KEY_CODES.ESCAPE]: clearSuggestions,
[KEY_CODES.PAGE_DOWN]: pageDown,
[KEY_CODES.PAGE_UP]: pageUp
};
if (keyOperation[e.keyCode]) {
keyOperation[e.keyCode]();
} else {
setSelectedIndex(-1);
}
}
return {
bindOption: {
onClick: e => {
let nodes = Array.from(listRef.current.children);
selectOption(nodes.indexOf(e.target.closest('li')));
}
},
bindInput: {
value: textValue,
onChange: e => onTextChange(e.target.value),
onKeyDown
},
bindOptions: {
ref: listRef
},
isBusy,
suggestions,
selectedIndex
};
}

View File

@ -5,11 +5,13 @@ import commonTranslationsEN from './locales/en/common.json'
import homeTranslationsEN from './locales/en/home.json'
import peopleTranslationsEN from './locales/en/people.json'
import authTranslationsEN from './locales/en/auth.json'
import sessTranslationsEN from './locales/en/sessions.json'
import commonTranslationsES from './locales/es/common.json'
import homeTranslationsES from './locales/es/home.json'
import peopleTranslationsES from './locales/es/people.json'
import authTranslationsES from './locales/es/auth.json'
import sessTranslationsES from './locales/es/sessions.json'
i18n.use(initReactI18next).init({
fallbackLng: 'en',
@ -20,14 +22,16 @@ i18n.use(initReactI18next).init({
common: commonTranslationsEN,
home: homeTranslationsEN,
people: peopleTranslationsEN,
auth: authTranslationsEN
auth: authTranslationsEN,
sessions: sessTranslationsEN,
},
es: {
//translations: require('./locales/es/translations.json')
common: commonTranslationsES,
home: homeTranslationsES,
people: peopleTranslationsES,
auth: authTranslationsES
auth: authTranslationsES,
sessions: sessTranslationsES,
}
},
//ns: ['translations'],

View File

@ -0,0 +1,20 @@
{
"new": {
"page_title": "Create Session",
"privacy": "Session Privacy",
"privacy_help": "Public sessions can be seen in the Browse Sessions feature by other musicians in the JamKazam community, and others can join your session. If you choose the “Private - only invited musicians can join” option, your session will not be visible to the community as a whole, and only those musicians you invite will be able to see or join your session. If you choose the “Private anyone can request to join, but requires approval” option, your session will be visible to the community in the Browse Sessions feature, and non-invited musicians may request to join your session, but you will have to grant permission per user to allow users into your session.",
"privacy_opt_public": "Public anyone can join",
"privacy_opt_private_invite": "Private only invited musicians can join",
"privacy_opt_private_approve": "Private anyone can request to join, but requires approval",
"invitations": "Session Invitations",
"invitations_help": "If you invite other users to join your session, this will generate an in-app notification and in some cases also an email invitation to invitees. Invited users will also see your session in the “For Me” section of the Browse Sessions feature. Invited users can join your session without further permission or action on your part, regardless of whether your session is public or private.",
"invitations_filter_placeholder": "Enter friend name",
"choose_friends": "Choose Friends",
"add_friends": "Add Friends",
"cancel": "Cancel",
"description": "Session Description",
"description_help": "If youre creating a public session, we strongly recommend that you enter a description of your session for example, what kinds of music youre interested in playing. This description will be displayed next to your session in the Browse Sessions feature, which will help other musicians in the community understand if your session is a good fit for them.",
"description_placeholder": "Enter session description. Recommended for public sessions to attract other musicians and them know what to expect in your session.",
"create_session": "Create Session"
}
}

View File

@ -0,0 +1,3 @@
{
"page_title": "Página de inicio"
}