semantic-ui-react#Popup TypeScript Examples
The following examples show how to use
semantic-ui-react#Popup.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: topmenu.tsx From website with MIT License | 6 votes |
useRightItems = ({ onMessageClicked }) => {
return (<>
<Menu.Item onClick={() => (window as any).swapThemeCss()}>
<Icon name='adjust' />
</Menu.Item>
<Menu.Item>
<Popup position='bottom center' flowing hoverable trigger={<Icon name='dollar' />}>
<p>We have enough reserve funds for now!</p>
<p>
Monthly cost <b>$15</b>, reserve fund <b>$205</b>
</p>
<p>
You can join our <a href='https://www.patreon.com/Datacore'>Patreon</a> for future funding rounds.
</p>
</Popup>
</Menu.Item>
<Menu.Item>
<Button size='tiny' color='green' onClick={onMessageClicked} content={'Developers needed!'} />
</Menu.Item>
<Menu.Item onClick={() => window.open('https://github.com/stt-datacore/website', '_blank')}>
<Icon name='github' />
</Menu.Item>
</>);
}
Example #2
Source File: crewpopup.tsx From website with MIT License | 6 votes |
render() {
const { crew, useBase } = this.props;
//console.log(crew);
if (!crew || !crew.symbol) {
return <span>ERROR!</span>;
}
return (
<Popup trigger={<span style={{ cursor: 'help', fontWeight: 'bolder' }}>{crew.name}</span>}>
<Popup.Header>{crew.name}</Popup.Header>
<Popup.Content>
<Image size='small' src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`} />
<Rating icon='star' defaultRating={crew.rarity} maxRating={crew.max_rarity} />
<p>{formatCrewStats(crew, useBase ?? true)}</p>
</Popup.Content>
</Popup>
);
}
Example #3
Source File: TopBar.tsx From watchparty with MIT License | 6 votes |
render() {
return (
<Popup
content="Create a new room with a random URL that you can share with friends"
trigger={
<Button
color="blue"
size={this.props.size as any}
icon
labelPosition="left"
onClick={this.createRoom}
className="toolButton"
fluid
>
<Icon name="certificate" />
New Room
</Button>
}
/>
);
}
Example #4
Source File: PrereqTree.tsx From peterportal-client with MIT License | 6 votes |
Node: FC<NodeProps> = (props) => {
return (
<div style={{ padding: '1px 0' }} className={`node-container ${props.node}`} key={props.index}>
<Popup
trigger={
<a href={'/course/' + props.label.replace(/\s+/g, '')} role='button' style={{ padding: '0.5rem' }} className={'node ui button'}>
{props.label}
</a>
}
content={props.content} basic position='top center' wide='very' />
</div>
);
}
Example #5
Source File: SpeakerControl.tsx From FLECT_Amazon_Chime_Meeting with Apache License 2.0 | 5 votes |
render() {
const props = this.props as any
const gs = this.props as GlobalState
const appState = props.appState as AppState
const outputAudioDevicesOpts=gs.outputAudioDevices!.map(info => { return { key: info.label, text: info.label, value: info.deviceId } })
const enableIcon=appState.currentSettings.speakerEnable ?
(
<Popup
trigger={
<Icon size="large" name="sound" color="black" link onClick={() => { props.toggleSpeaker() }}/>
}
content="disable."
/>
)
:
(
<Popup
trigger={
<Icon.Group link onClick={() => { props.toggleSpeaker() }}>
<Icon size="large" color='black' name='sound' />
<Icon size="large" color='red' name='dont' />
</Icon.Group>
}
content="enable."
/>
)
return (
<Grid>
<Grid.Row>
<Grid.Column >
{enableIcon}
<Dropdown
style={{paddingLeft:"10px"}}
pointing='top left'
options={outputAudioDevicesOpts}
trigger={trigger}
onChange={(e, { value }) => props.selectOutputAudioDevice(value as string)}
/>
{/* <List style={{paddingLeft:"15px",paddingTop:"0px",paddingBottom:"0px"}} link>
<List.Item as='a' active onClick={() => { props.toggleSpeaker() }}><Icon name="ban" color={appState.currentSettings.speakerEnable ? "grey" : "red"}/>Disable Speaker</List.Item>
</List> */}
</Grid.Column>
</Grid.Row>
</Grid>
)
}
Example #6
Source File: crewpage.tsx From website with MIT License | 5 votes |
renderEquipmentDetails(crew) {
if (!this.state.selectedEquipment) {
return <span />;
}
let es = crew.equipment_slots.find(es => es.symbol === this.state.selectedEquipment);
let equipment = this.state.items.find(item => item.symbol === es.symbol);
if (!equipment) {
console.error('Could not find equipment for slot', es);
return <span />;
}
if (!equipment.recipe) {
return (
<div>
<br />
<p>This item is not craftable, you can find it in these sources:</p>
<ItemSources item_sources={equipment.item_sources} />
</div>
);
}
return (
<div>
<Grid columns={4} centered padded>
{equipment.recipe.list.map(entry => {
let recipeEntry = this.state.items.find(item => item.symbol === entry.symbol);
return (
<Grid.Column key={recipeEntry.name + recipeEntry.rarity} textAlign='center'>
<Popup
trigger={
<Label as='a' style={{ background: CONFIG.RARITIES[recipeEntry.rarity].color }} image size='big'>
<img src={`${process.env.GATSBY_ASSETS_URL}${recipeEntry.imageUrl}`} />x{entry.count}
</Label>
}
header={CONFIG.RARITIES[recipeEntry.rarity].name + ' ' + recipeEntry.name}
content={<ItemSources item_sources={recipeEntry.item_sources} />}
wide
/>
</Grid.Column>
);
})}
</Grid>
</div>
);
}
Example #7
Source File: bigbook2.tsx From website with MIT License | 5 votes |
renderCrew(entry): JSX.Element {
let markdownRemark = {
frontmatter: {
bigbook_tier: entry.bcrew.bigbook_tier,
events: entry.bcrew.events,
in_portal: entry.bcrew.in_portal
}
};
return (
<Grid.Column key={entry.crew.symbol}>
<div style={{ textAlign: 'center', fontSize: '0.75em' }}>
<Popup
trigger={
<Image
src={`${process.env.GATSBY_ASSETS_URL}${entry.crew.imageUrlPortrait}`}
size='small'
style={{
borderColor: CONFIG.RARITIES[entry.crew.max_rarity].color,
borderWidth: '1px',
borderRadius: '4px',
borderStyle: 'solid'
}}
/>
}
wide='very'
on='click'>
<Header>
<Link to={`/crew/${entry.crew.symbol}/`}>
{entry.crew.name}{' '}
<Rating rating={entry.crew.max_rarity} maxRating={entry.crew.max_rarity} icon='star' size='large' disabled />
</Link>
</Header>
<CommonCrewData crew={entry.crew} markdownRemark={markdownRemark} />
<div dangerouslySetInnerHTML={{ __html: marked(entry.bcrew.markdownContent) }} />
</Popup>
<Link to={`/crew/${entry.crew.symbol}/`}>{entry.crew.name}</Link>
</div>
</Grid.Column>
);
}
Example #8
Source File: voyagestats.tsx From website with MIT License | 5 votes |
_renderCrew() {
const {voyageData} = this.props;
const ship = this.ship;
return (
<div>
{ship && (<span>Ship : <b>{ship.name}</b></span>)}
<Grid columns={isMobile ? 1 : 2}>
<Grid.Column>
<ul>
{Object.values(CONFIG.VOYAGE_CREW_SLOTS).map((entry, idx) => {
let { crew, name } = Object.values(voyageData.crew_slots).find(slot => slot.symbol == entry);
if (!crew.imageUrlPortrait)
crew.imageUrlPortrait =
`${crew.portrait.file.substring(1).replaceAll('/', '_')}.png`;
return (
<li key={idx}>
{name}
{' : '}
<CrewPopup crew={crew} useBase={false} />
</li>
);
})}
</ul>
</Grid.Column>
<Grid.Column verticalAlign="middle">
<ul>
<li>
Antimatter
{' : '}
<b>{voyageData.max_hp}</b>
</li>
</ul>
<ul>
{Object.keys(CONFIG.SKILLS).map((entry, idx) => {
const agg = voyageData.skill_aggregates[entry];
if (typeof(agg) === 'number') {
return (<li key={idx}>{`${CONFIG.SKILLS[entry]} : ${Math.round(agg)}`}</li>);
} else {
const score = Math.floor(agg.core + (agg.range_min + agg.range_max)/2);
return (
<li key={idx}>
{CONFIG.SKILLS[entry]}
{' : '}
<Popup wide trigger={<span style={{ cursor: 'help', fontWeight: 'bolder' }}>{score}</span>}>
<Popup.Content>
{agg.core + ' +(' + agg.range_min + '-' + agg.range_max + ')'}
</Popup.Content>
</Popup>
</li>
);
}
})}
</ul>
</Grid.Column>
</Grid>
</div>
);
}
Example #9
Source File: voyagecalculator.tsx From website with MIT License | 5 votes |
VoyageExisting = (props: VoyageExistingProps) => {
const { voyageConfig, allShips, useCalc } = props;
const [CIVASExportFailed, setCIVASExportFailed] = React.useState(false);
const [doingCIVASExport, setDoingCIVASExport] = React.useState(false);
const hoursToTime = hours => {
let wholeHours = Math.floor(hours);
return `${wholeHours}:${Math.floor((hours-wholeHours)*60).toString().padStart(2, '0')}`
}
const exportData = () => new Promise((resolve, reject) => {
setDoingCIVASExport(true);
let estimate = getEstimate({
startAm: voyageConfig.max_hp,
ps: voyageConfig.skill_aggregates[voyageConfig.skills['primary_skill']],
ss: voyageConfig.skill_aggregates[voyageConfig.skills['secondary_skill']],
others: Object.values(voyageConfig.skill_aggregates).filter(s => !Object.values(voyageConfig.skills).includes(s.skill)),
}, () => true).refills[0].result;
let values = [
new Date(voyageConfig.created_at).toLocaleDateString(),
hoursToTime(estimate),
hoursToTime(voyageConfig.log_index/180),
voyageConfig.hp
];
values = values.concat(voyageConfig
.crew_slots
.sort((s1, s2) => CONFIG.VOYAGE_CREW_SLOTS.indexOf(s1.symbol) - CONFIG.VOYAGE_CREW_SLOTS.indexOf(s2.symbol))
.map(s => s.crew.name)
);
navigator.clipboard.writeText(values.join('\n')).then(resolve, reject);
});
return (
<div style={{ marginTop: '1em' }}>
<VoyageStats
voyageData={voyageConfig}
ships={allShips}
showPanels={voyageConfig.state == 'started' ? ['estimate'] : ['rewards']}
/>
<Button onClick={() => useCalc()}>Return to crew calculator</Button>
{(voyageConfig.state == 'recalled' || voyageConfig.state == 'failed') && navigator.clipboard &&
<React.Fragment>
<Button loading={doingCIVASExport} onClick={() => exportData().then(
() => setDoingCIVASExport(false),
() => {
setDoingCIVASExport(false);
setCIVASExportFailed(true);
let timeout = setTimeout(() => {
setCIVASExportFailed(false);
clearTimeout(timeout);
}, 5000);
})}>
Export to CIVAS
</Button>
<Popup
trigger={<Icon name='help' />}
content={
<>
Copies details of the voyage to the clipboard so that it can be pasted into <a href='https://docs.google.com/spreadsheets/d/1nbnD2WvDXAT9cxEWep0f78bv6_hOaP51tmRjmY0oT1k' target='_blank'>Captain Idol's Voyage Analysis Sheet</a>
</>
}
mouseLeaveDelay={1000}
/>
{CIVASExportFailed &&
<Message negative>Export to clipboard failed</Message>
}
</React.Fragment>
}
</div>
)
}
Example #10
Source File: crewfullequiptree.tsx From website with MIT License | 5 votes |
render() {
const { crew, items } = this.props;
if (!crew || !this.props.visible) {
return <span />;
}
let { craftCost, demands, factionOnlyTotal, totalChronCost } = calculateCrewDemands(crew, items);
return (
<Modal open={this.props.visible} onClose={() => this.props.onClosed()}>
<Modal.Header>{crew.name}'s expanded equipment recipe trees</Modal.Header>
<Modal.Content scrolling>
<p>
Faction-only items required <b>{factionOnlyTotal}</b>
</p>
<p>
Estimated chroniton cost{' '}
<span style={{ display: 'inline-block' }}>
<img src={`${process.env.GATSBY_ASSETS_URL}atlas/energy_icon.png`} height={14} />
</span>{' '}
<b>{totalChronCost}</b>
<Popup
wide
trigger={<Icon fitted name='help' />}
header={'How is this calculated?'}
content={
<div>
<p>This sums the estimated chroniton cost of each equipment and component in the tree.</p>
<p>It estimates an item's cost by running the formula below for each mission and choosing the cheapest:</p>
<p>
<code>
(6 - PIPS) * 1.8 * <i>mission cost</i>
</code>
</p>
<p>See code for details. Feedback is welcome!</p>
</div>
}
/>
</p>
<p>
Build cost{' '}
<span style={{ display: 'inline-block' }}>
<img src={`${process.env.GATSBY_ASSETS_URL}currency_sc_currency_0.png`} height={16} />
</span>{' '}
<b>{craftCost}</b>
</p>
<Grid columns={3} centered padded>
{demands.map((entry, idx) => (
<Grid.Column key={idx}>
<Popup
trigger={
<Header
style={{ display: 'flex', cursor: 'zoom-in' }}
icon={
<ItemDisplay
src={`${process.env.GATSBY_ASSETS_URL}${entry.equipment.imageUrl}`}
size={48}
maxRarity={entry.equipment.rarity}
rarity={entry.equipment.rarity}
/>
}
content={entry.equipment.name}
subheader={`Need ${entry.count} ${entry.factionOnly ? ' (FACTION)' : ''}`}
/>
}
header={CONFIG.RARITIES[entry.equipment.rarity].name + ' ' + entry.equipment.name}
content={<ItemSources item_sources={entry.equipment.item_sources} />}
on='click'
wide
/>
</Grid.Column>
))}
</Grid>
</Modal.Content>
</Modal>
);
}
Example #11
Source File: cabexplanation.tsx From website with MIT License | 5 votes |
CABExplanation = () => (
<Popup wide trigger={<Icon name="help" />} header={'CAB STT Power Ratings'} content={text}/>
)
Example #12
Source File: MicControl.tsx From FLECT_Amazon_Chime_Meeting with Apache License 2.0 | 5 votes |
render() {
const props = this.props as any
const gs = this.props as GlobalState
const appState = props.appState as AppState
const inputAudioDevicesOpts=gs.inputAudioDevices!.map(info => { return { key: info.label, text: info.label, value: info.deviceId } })
const muteIcon=appState.currentSettings.mute ?
(
<Popup
trigger={
<Icon.Group link onClick={() => { props.toggleMute() }}>
<Icon size="large" color='black' name='microphone' />
<Icon size="large" color='red' name='dont' />
</Icon.Group>
}
content="unmute."
/>
)
:
(
<Popup
trigger={
<Icon size="large" name="microphone" color="black" link onClick={() => { props.toggleMute() }}/>
}
content="mute."
/>
)
return (
<Grid>
<Grid.Row>
<Grid.Column >
{muteIcon}
<Dropdown
style={{paddingLeft:"10px"}}
pointing='top left'
options={inputAudioDevicesOpts}
trigger={trigger}
onChange={(e, { value }) => props.selectInputAudioDevice(value as string)}
/>
{/* <List style={{paddingLeft:"15px",paddingTop:"0px",paddingBottom:"0px"}} link>
<List.Item as='a' active onClick={() => { props.toggleMute() }}><Icon name="ban" color={appState.currentSettings.mute ? "red" : "grey"} />Mute</List.Item>
</List> */}
</Grid.Column>
</Grid.Row>
</Grid>
)
}
Example #13
Source File: TopBar.tsx From watchparty with MIT License | 5 votes |
render() {
if (this.props.user) {
return (
<div
style={{
margin: '4px',
width: '100px',
alignItems: 'center',
cursor: 'pointer',
}}
>
<Image
avatar
src={this.state.userImage}
onClick={() => this.setState({ isProfileOpen: true })}
/>
{this.state.isProfileOpen && this.props.user && (
<ProfileModal
user={this.props.user}
userImage={this.state.userImage}
close={() => this.setState({ isProfileOpen: false })}
/>
)}
</div>
);
}
return (
<React.Fragment>
{this.state.isLoginOpen && (
<LoginModal
closeLogin={() => this.setState({ isLoginOpen: false })}
/>
)}
<Popup
basic
content="Sign in to set your name and picture, subscribe, or launch VBrowsers"
trigger={
<Dropdown
style={{ height: '36px' }}
icon="sign in"
labeled
className="icon"
button
text="Sign in"
fluid={this.props.fluid}
>
<Dropdown.Menu>
<Dropdown.Item onClick={this.facebookSignIn}>
<Icon name="facebook" />
Facebook
</Dropdown.Item>
<Dropdown.Item onClick={this.googleSignIn}>
<Icon name="google" />
Google
</Dropdown.Item>
<Dropdown.Item
onClick={() => this.setState({ isLoginOpen: true })}
>
<Icon name="mail" />
Email
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
}
/>
</React.Fragment>
);
}
Example #14
Source File: SubscribeButton.tsx From watchparty with MIT License | 5 votes |
SubscribeButton = ({
user,
isSubscriber,
isCustomer,
}: {
user: firebase.User | undefined;
isSubscriber: boolean;
isCustomer: boolean;
}) => {
const [isSubscribeModalOpen, setIsSubscribeModalOpen] = useState(false);
const onManage = useCallback(async () => {
const resp = await window.fetch(serverPath + '/manageSub', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
uid: user?.uid,
token: await user?.getIdToken(),
return_url: window.location.href,
}),
});
const session = await resp.json();
console.log(session);
window.location.assign(session.url);
}, [user]);
return (
<>
{isSubscribeModalOpen && (
<SubscribeModal
user={user}
isSubscriber={isSubscriber}
closeSubscribe={() => setIsSubscribeModalOpen(false)}
/>
)}
{!isSubscriber && (
<Popup
content="Subscribe to help support us and enable additional features!"
trigger={
<Button
fluid
color="orange"
className="toolButton"
icon
labelPosition="left"
onClick={() => setIsSubscribeModalOpen(true)}
>
<Icon name="plus" />
Subscribe
</Button>
}
/>
)}
{isSubscriber && (
<Popup
content="Manage your subscription"
trigger={
<Button
fluid
color="orange"
className="toolButton"
icon
labelPosition="left"
onClick={onManage}
>
<Icon name="wrench" />
Manage
</Button>
}
/>
)}
</>
);
}
Example #15
Source File: VideoControl.tsx From FLECT_Amazon_Chime_Meeting with Apache License 2.0 | 5 votes |
render() {
const props = this.props as any
const gs = this.props as GlobalState
const appState = props.appState as AppState
const inputVideoDevicesOpts=gs.inputVideoDevices!.map(info => { return { key: info.label, text: info.label, value: info.deviceId } })
const enableIcon=appState.currentSettings.videoEnable ?
(
<Popup
trigger={
<Icon size="large" name="video camera" color="black" link onClick={() => { props.toggleVideo() }}/>
}
content="disable."
/>
)
:
(
<Popup
trigger={
<Icon.Group link onClick={() => { props.toggleVideo() }}>
<Icon size="large" color='black' name='video camera' />
<Icon size="large" color='red' name='dont' />
</Icon.Group>
}
content="enable."
/>
)
return (
<Grid>
<Grid.Row>
<Grid.Column>
{enableIcon}
<Dropdown
style={{paddingLeft:"10px"}}
pointing='top left'
options={inputVideoDevicesOpts}
trigger={trigger}
onChange={(e, { value }) => props.selectInputVideoDevice(value as string)}
/>
{/* <List style={{paddingLeft:"15px",paddingTop:"0px",paddingBottom:"0px"}} link>
<List.Item as='a' active onClick={() => { props.toggleVideo() }}><Icon name="ban" color={appState.currentSettings.videoEnable ? "grey" : "red"}/>Disable Camera</List.Item>
</List> */}
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column>
<List link>
<List.Item as='a' active onClick={() => this.fileInputRef.current!.click()}>
<Icon name="folder" active />dummy mov.
</List.Item>
</List>
<input
ref={this.fileInputRef}
type="file"
hidden
onChange={(e) => props.setSelectedVideo(e)}
/>
</Grid.Column>
</Grid.Row>
</Grid>
)
}
Example #16
Source File: searchabletable.tsx From website with MIT License | 4 votes |
SearchableTable = (props: SearchableTableProps) => {
let data = [...props.data];
const tableId = props.id ?? '';
const hasDate = data.length > 0 && data[0].date_added;
const defaultSort = {
column: data.length > 0 && hasDate ? 'date_added' : 'name',
direction: data.length > 0 && hasDate ? 'descending' : 'ascending'
};
const [searchFilter, setSearchFilter] = useStateWithStorage(tableId+'searchFilter', '');
const [filterType, setFilterType] = useStateWithStorage(tableId+'filterType', 'Any match');
const [column, setColumn] = useStateWithStorage(tableId+'column', defaultSort.column);
const [direction, setDirection] = useStateWithStorage(tableId+'direction', defaultSort.direction);
const [pagination_rows, setPaginationRows] = useStateWithStorage(tableId+'paginationRows', 10);
const [pagination_page, setPaginationPage] = useStateWithStorage(tableId+'paginationPage', 1);
const [activeLock, setActiveLock] = React.useState(undefined);
// Override stored values with custom initial options and reset all others to defaults
// Previously stored values will be rendered before an override triggers a re-render
React.useEffect(() => {
if (props.initOptions) {
setSearchFilter(props.initOptions['search'] ?? '');
setFilterType(props.initOptions['filter'] ?? 'Any match');
setColumn(props.initOptions['column'] ?? defaultSort.column);
setDirection(props.initOptions['direction'] ?? defaultSort.direction);
setPaginationRows(props.initOptions['rows'] ?? 10);
setPaginationPage(props.initOptions['page'] ?? 1);
}
}, [props.initOptions]);
// Activate lock by default if only 1 lockable
React.useEffect(() => {
setActiveLock(props.lockable?.length === 1 ? props.lockable[0] : undefined);
}, [props.lockable]);
// Update column and/or toggle direction, and store new values in state
// Actual sorting of full dataset will occur on next render before filtering and pagination
function onHeaderClick(newColumn) {
if (!newColumn.column) return;
const lastColumn = column, lastDirection = direction;
const sortConfig = {
field: newColumn.column,
direction: lastDirection === 'ascending' ? 'descending' : 'ascending'
};
if (newColumn.pseudocolumns && newColumn.pseudocolumns.includes(lastColumn)) {
if (direction === 'descending') {
const nextIndex = newColumn.pseudocolumns.indexOf(lastColumn) + 1; // Will be 0 if previous column was not a pseudocolumn
sortConfig.field = newColumn.pseudocolumns[nextIndex === newColumn.pseudocolumns.length ? 0 : nextIndex];
sortConfig.direction = 'ascending';
}
else {
sortConfig.field = lastColumn;
sortConfig.direction = 'descending';
}
}
else if (newColumn.column !== lastColumn) {
sortConfig.direction = newColumn.reverse ? 'descending' : 'ascending';
}
setColumn(sortConfig.field);
setDirection(sortConfig.direction);
setPaginationPage(1);
}
function onChangeFilter(value) {
setSearchFilter(value);
setPaginationPage(1);
}
function renderTableHeader(column: any, direction: 'descending' | 'ascending' | null): JSX.Element {
return (
<Table.Row>
{props.config.map((cell, idx) => (
<Table.HeaderCell
key={idx}
width={cell.width as any}
sorted={((cell.pseudocolumns && cell.pseudocolumns.includes(column)) || (column === cell.column)) ? direction : null}
onClick={() => onHeaderClick(cell)}
textAlign={cell.width === 1 ? 'center' : 'left'}
>
{cell.title}{cell.pseudocolumns?.includes(column) && <><br/><small>{column.replace('_',' ').replace('.length', '')}</small></>}
</Table.HeaderCell>
))}
</Table.Row>
);
}
function renderPermalink(): JSX.Element {
// Will not catch custom options (e.g. highlight)
const params = new URLSearchParams();
if (searchFilter != '') params.append('search', searchFilter);
if (filterType != 'Any match') params.append('filter', filterType);
if (column != defaultSort.column) params.append('column', column);
if (direction != defaultSort.direction) params.append('direction', direction);
if (pagination_rows != 10) params.append('rows', pagination_rows);
if (pagination_page != 1) params.append('page', pagination_page);
let permalink = window.location.protocol + '//' + window.location.host + window.location.pathname;
if (params.toString() != '') permalink += '?' + params.toString();
return (
<Link to={permalink}>
<Icon name='linkify' /> Permalink
</Link>
);
}
function onLockableClick(lock: any): void {
if (lock) {
setActiveLock(lock);
}
else {
setActiveLock(undefined);
// Remember active page after removing lock
setPaginationPage(activePage);
}
}
function isRowActive(row: any, highlight: any): boolean {
if (!highlight) return false;
let isMatch = true;
Object.keys(highlight).forEach(key => {
if (row[key] !== highlight[key]) isMatch = false;
});
return isMatch;
}
// Sorting
if (column) {
const sortConfig: IConfigSortData = {
field: column,
direction: direction,
keepSortOptions: true
};
// Define tiebreaker rules with names in alphabetical order as default
// Hack here to sort rarity in the same direction as max_rarity
let subsort = [];
const columnConfig = props.config.find(col => col.column === column);
if (columnConfig && columnConfig.tiebreakers) {
subsort = columnConfig.tiebreakers.map(subfield => {
const subdirection = subfield.substr(subfield.length-6) === 'rarity' ? direction : 'ascending';
return { field: subfield, direction: subdirection };
});
}
if (column != 'name') subsort.push({ field: 'name', direction: 'ascending' });
sortConfig.subsort = subsort;
// Use original dataset for sorting
const sorted: IResultSortDataBy = sortDataBy([...props.data], sortConfig);
data = sorted.result;
// Sorting by pre-calculated ranks should filter out crew without matching skills
// Otherwise crew without skills show up first (because 0 comes before 1)
if (column.substr(0, 5) === 'ranks') {
const rank = column.split('.')[1];
data = data.filter(row => row.ranks[rank] > 0);
}
}
// Filtering
let filters = [];
if (searchFilter) {
let grouped = searchFilter.split(/\s+OR\s+/i);
grouped.forEach(group => {
filters.push(SearchString.parse(group));
});
}
data = data.filter(row => props.filterRow(row, filters, filterType));
// Pagination
let activePage = pagination_page;
if (activeLock) {
const index = data.findIndex(row => isRowActive(row, activeLock));
// Locked crew is not viewable in current filter
if (index < 0) {
setActiveLock(undefined);
return (<></>);
}
activePage = Math.floor(index / pagination_rows) + 1;
}
let totalPages = Math.ceil(data.length / pagination_rows);
if (activePage > totalPages) activePage = totalPages;
data = data.slice(pagination_rows * (activePage - 1), pagination_rows * activePage);
return (
<div>
<Input
style={{ width: isMobile ? '100%' : '50%' }}
iconPosition="left"
placeholder="Search..."
value={searchFilter}
onChange={(e, { value }) => onChangeFilter(value)}>
<input />
<Icon name='search' />
<Button icon onClick={() => onChangeFilter('')} >
<Icon name='delete' />
</Button>
</Input>
{props.showFilterOptions && (
<span style={{ paddingLeft: '2em' }}>
<Dropdown inline
options={filterTypeOptions}
value={filterType}
onChange={(event, {value}) => setFilterType(value as number)}
/>
</span>
)}
<Popup wide trigger={<Icon name="help" />}
header={'Advanced search'}
content={props.explanation ? props.explanation : renderDefaultExplanation()}
/>
{props.lockable && <LockButtons lockable={props.lockable} activeLock={activeLock} setLock={onLockableClick} />}
<Table sortable celled selectable striped collapsing unstackable compact="very">
<Table.Header>{renderTableHeader(column, direction)}</Table.Header>
<Table.Body>{data.map((row, idx) => props.renderTableRow(row, idx, isRowActive(row, activeLock)))}</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan={props.config.length}>
<Pagination
totalPages={totalPages}
activePage={activePage}
onPageChange={(event, { activePage }) => {
setPaginationPage(activePage as number);
setActiveLock(undefined); // Remove lock when changing pages
}}
/>
<span style={{ paddingLeft: '2em'}}>
Rows per page:{' '}
<Dropdown
inline
options={pagingOptions}
value={pagination_rows}
onChange={(event, {value}) => {
setPaginationPage(1);
setPaginationRows(value as number);
}}
/>
</span>
{props.showPermalink && (<span style={{ paddingLeft: '5em'}}>{renderPermalink()}</span>)}
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</div>
);
}
Example #17
Source File: playertools.tsx From website with MIT License | 4 votes |
PlayerToolsPanes = (props: PlayerToolsPanesProps) => {
const { playerData, strippedPlayerData, voyageData, eventData, activeCrew, dataSource,
allCrew, allItems, requestShowForm, requestClearData } = props;
const [showIfStale, setShowIfStale] = useStateWithStorage('tools/showStale', true);
const [showShare, setShowShare] = useStateWithStorage(playerData.player.dbid+'/tools/showShare', true, { rememberForever: true, onInitialize: variableReady });
const [profileAutoUpdate, setProfileAutoUpdate] = useStateWithStorage(playerData.player.dbid+'/tools/profileAutoUpdate', false, { rememberForever: true });
const [profileUploaded, setProfileUploaded] = React.useState(false);
const [profileUploading, setProfileUploading] = React.useState(false);
const [profileShared, setProfileShared] = useStateWithStorage('tools/profileShared', false);
const [varsReady, setVarsReady] = React.useState(false);
const [activeTool, setActiveTool] = React.useState('voyage');
React.useEffect(() => {
if (dataSource == 'input' && profileAutoUpdate && !profileUploaded) {
console.log('Uploading profile');
shareProfile();
}
}, [profileAutoUpdate, strippedPlayerData]);
const tools = playerTools;
React.useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('tool') && tools[urlParams.get('tool')])
setActiveTool(urlParams.get('tool'));
}, [window.location.search]);
const StaleMessage = () => {
const STALETHRESHOLD = 3; // in hours
if (showIfStale && new Date().getTime()-playerData.calc.lastModified.getTime() > STALETHRESHOLD*60*60*1000) {
return (
<Message
warning
icon='clock'
header='Update your player data'
content="It's been a few hours since you last updated your player data. We recommend that you update now to make sure our tools are providing you recent information about your crew."
onDismiss={() => setShowIfStale(false)}
/>
);
}
else {
return (<></>);
}
};
const ShareMessage = () => {
if (!showShare) return (<></>);
// The option to auto-share profile only appears after a profile is uploaded or if previously set to auto-update
const bShowUploaded = profileUploaded || profileAutoUpdate;
return (
<Message icon onDismiss={() => setShowShare(false)}>
<Icon name='share alternate' />
<Message.Content>
<Message.Header>Share your player profile!</Message.Header>
{!bShowUploaded && (
<div>
<p>
Click here to{' '}
<Button size='small' color='green' onClick={() => shareProfile()}>
{profileUploading && <Icon loading name='spinner' />}share your profile
</Button>{' '}
and unlock more tools and export options for items and ships. More details:
</p>
<Message.List>
<Message.Item>
Once shared, the profile will be publicly accessible, will be accessible by your DBID link, and linked on related pages (such as fleet pages & event pages)
</Message.Item>
<Message.Item>
There is no private information included in the player profile; information being shared is limited to:{' '}
<b>captain name, level, vip level, fleet name and role, achievements, completed missions, your crew, items and ships.</b>
</Message.Item>
</Message.List>
</div>
)}
{bShowUploaded && (
<Form.Group>
<p>
Your profile was uploaded. Share the link:{' '}
<a
href={`${process.env.GATSBY_DATACORE_URL}profile/?dbid=${playerData.player.dbid}`}
target='_blank'>{`${process.env.GATSBY_DATACORE_URL}profile/?dbid=${playerData.player.dbid}`}</a>
</p>
<Form.Field
control={Checkbox}
label='Automatically share profile after every import'
checked={profileAutoUpdate}
onChange={(e, { checked }) => setProfileAutoUpdate(checked)}
/>
</Form.Group>
)}
</Message.Content>
</Message>
);
};
if (!varsReady)
return (<PlayerToolsLoading />);
const PlayerLevelProgress = () => {
const endingValue = playerData.player.character.xp_for_next_level - playerData.player.character.xp_for_current_level;
const currentValue = playerData.player.character.xp - playerData.player.character.xp_for_current_level;
const percent = (currentValue / endingValue) * 100;
return (
<Progress
percent={percent.toPrecision(3)}
label={`Level ${playerData.player.character.level}: ${playerData.player.character.xp} / ${playerData.player.character.xp_for_next_level}`}
progress
/>
);
};
return (
<Layout title='Player tools'>
<Header as='h4'>Hello, {playerData.player.character.display_name}</Header>
<PlayerLevelProgress />
<StaleMessage />
<Menu compact stackable>
<Menu.Item>
Last imported: {playerData.calc.lastModified.toLocaleString()}
</Menu.Item>
<Dropdown item text='Profile options'>
<Dropdown.Menu>
<Dropdown.Item onClick={() => requestShowForm(true)}>Update now...</Dropdown.Item>
{!showShare && (<Dropdown.Item onClick={() => setShowShare(true)}>Share profile...</Dropdown.Item>)}
<Dropdown.Item onClick={() => requestClearData()}>Clear player data</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Dropdown item text='Export'>
<Dropdown.Menu>
<Popup basic content='Download crew data as traditional comma delimited CSV file' trigger={
<Dropdown.Item onClick={() => exportCrewTool()} content='Download CSV...' />
} />
<Popup basic content='Copy crew data to clipboard in Google Sheets format' trigger={
<Dropdown.Item onClick={() => exportCrewToClipboard()} content='Copy to clipboard' />
} />
</Dropdown.Menu>
</Dropdown>
</Menu>
<React.Fragment>
<ShareMessage />
<Header as='h3'>{tools[activeTool].title}</Header>
{tools[activeTool].render(props)}
</React.Fragment>
</Layout>
);
function variableReady(keyName: string) {
setVarsReady(true);
}
function shareProfile() {
setProfileUploading(true);
let jsonBody = JSON.stringify({
dbid: playerData.player.dbid,
player_data: strippedPlayerData
});
fetch(`${process.env.GATSBY_DATACORE_URL}api/post_profile`, {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: jsonBody
}).then(() => {
if (!profileAutoUpdate) window.open(`${process.env.GATSBY_DATACORE_URL}profile/?dbid=${playerData.player.dbid}`, '_blank');
setProfileUploading(false);
setProfileUploaded(true);
setProfileShared(true);
});
}
function exportCrewTool() {
let text = exportCrew(playerData.player.character.crew.concat(playerData.player.character.unOwnedCrew));
downloadData(`data:text/csv;charset=utf-8,${encodeURIComponent(text)}`, 'crew.csv');
}
function exportCrewToClipboard() {
let text = exportCrew(playerData.player.character.crew.concat(playerData.player.character.unOwnedCrew), '\t');
navigator.clipboard.writeText(text);
}
}
Example #18
Source File: item_info.tsx From website with MIT License | 4 votes |
render() {
const { errorMessage, item_data, items } = this.state;
if (item_data === undefined || errorMessage !== undefined) {
return (
<Layout title='Item information'>
<Header as="h4">Item information</Header>
{errorMessage && (
<Message negative>
<Message.Header>Unable to load item information</Message.Header>
<pre>{errorMessage.toString()}</pre>
</Message>
)}
{!errorMessage && (
<div>
<Icon loading name="spinner" /> Loading...
</div>
)}
</Layout>
);
}
console.log(item_data);
let bonusText = [];
if (item_data.item.bonuses) {
for (let [key, value] of Object.entries(item_data.item.bonuses)) {
let bonus = CONFIG.STATS_CONFIG[Number.parseInt(key)];
if (bonus) {
bonusText.push(`+${value} ${bonus.symbol}`);
} else {
// TODO: what kind of bonus is this?
}
}
}
// TODO: share this code with equipment.ts
let demands = [];
if (item_data.item.recipe) {
for (let iter of item_data.item.recipe.list) {
let recipeEquipment = items.find(item => item.symbol === iter.symbol);
demands.push({
count: iter.count,
symbol: iter.symbol,
equipment: recipeEquipment,
factionOnly: iter.factionOnly
});
}
}
return (
<Layout title={item_data.item.name}>
<Message icon warning>
<Icon name="exclamation triangle" />
<Message.Content>
<Message.Header>Work in progress!</Message.Header>
This section is under development and not fully functional yet.
</Message.Content>
</Message>
<Header as="h3">
{item_data.item.name}{' '}
<Rating icon='star' rating={item_data.item.rarity} maxRating={item_data.item.rarity} size="large" disabled />
</Header>
<Image size="small" src={`${process.env.GATSBY_ASSETS_URL}${item_data.item.imageUrl}`} />
<br />
{bonusText.length > 0 && (
<div>
<p>Bonuses: {bonusText.join(', ')}</p>
<br />
</div>
)}
{item_data.item.recipe && item_data.item.recipe.list && (
<div>
<Header as="h4">Craft it for {item_data.item.recipe.craftCost} chrons using this recipe:</Header>
<Grid columns={3} padded>
{demands.map((entry, idx) => (
<Grid.Column key={idx}>
<Popup
trigger={
<Header
style={{ display: 'flex', cursor: 'zoom-in' }}
icon={
<ItemDisplay
src={`${process.env.GATSBY_ASSETS_URL}${entry.equipment.imageUrl}`}
size={48}
maxRarity={entry.equipment.rarity}
rarity={entry.equipment.rarity}
/>
}
content={entry.equipment.name}
subheader={`Need ${entry.count} ${entry.factionOnly ? ' (FACTION)' : ''}`}
/>
}
header={
<Link to={`/item_info?symbol=${entry.symbol}`}>
{CONFIG.RARITIES[entry.equipment.rarity].name + ' ' + entry.equipment.name}
</Link>
}
content={<ItemSources item_sources={entry.equipment.item_sources} />}
on="click"
wide
/>
</Grid.Column>
))}
</Grid>
</div>
)}
{item_data.item.item_sources.length > 0 && (
<div>
<Header as="h4">Item sources</Header>
<ItemSources item_sources={item_data.item.item_sources} />
<br />
</div>
)}
{item_data.crew_levels.length > 0 && (
<div>
<Header as="h4">Equippable by this crew:</Header>
<Grid columns={3} padded>
{item_data.crew_levels.map((entry, idx) => (
<Grid.Column key={idx}>
<Header
style={{ display: 'flex' }}
icon={
<ItemDisplay
src={`${process.env.GATSBY_ASSETS_URL}${entry.crew.imageUrlPortrait}`}
size={60}
maxRarity={entry.crew.max_rarity}
rarity={entry.crew.max_rarity}
/>
}
content={<Link to={`/crew/${entry.crew.symbol}/`}>{entry.crew.name}</Link>}
subheader={`Level ${entry.level}`}
/>
</Grid.Column>
))}
</Grid>
</div>
)}
{item_data.builds.length > 0 && (
<div>
<Header as="h4">Is used to build these</Header>
<Grid columns={3} padded>
{item_data.builds.map((entry, idx) => (
<Grid.Column key={idx}>
<Header
style={{ display: 'flex', cursor: 'zoom-in' }}
icon={
<ItemDisplay
src={`${process.env.GATSBY_ASSETS_URL}${entry.imageUrl}`}
size={48}
maxRarity={entry.rarity}
rarity={entry.rarity}
/>
}
content={
<Link to={`/item_info?symbol=${entry.symbol}`}>
{CONFIG.RARITIES[entry.rarity].name + ' ' + entry.name}
</Link>
}
/>
</Grid.Column>
))}
</Grid>
</div>
)}
</Layout>
);
}
Example #19
Source File: crewchallenge.tsx From website with MIT License | 4 votes |
CrewChallengeGame = (props: CrewChallengeGame) => {
const { rules, solution, guesses, setGuesses, solveState, setSolveState } = props;
const portalCrew = React.useContext(PortalCrewContext);
const [solvedCrew, setSolvedCrew] = React.useState(undefined);
const [guessesEvaluated, setGuessesEvaluated] = React.useState([]);
React.useEffect(() => {
if (solution === '') return;
setSolvedCrew(getCrew(solution));
setGuessesEvaluated([]);
}, [solution]);
if (!solvedCrew) return (<></>);
const newEvaluations = [];
guesses.forEach(guess => {
if (!guessesEvaluated.find(evaluation => evaluation.symbol === guess)) {
const guessedCrew = getCrew(guess);
guessedCrew.evaluation = evaluateGuess(guessedCrew);
newEvaluations.push(guessedCrew);
}
});
if (newEvaluations.length > 0)
setGuessesEvaluated([...guessesEvaluated, ...newEvaluations]);
return (
<React.Fragment>
<GuessTable solveState={solveState} solvedCrew={solvedCrew} guessesEvaluated={guessesEvaluated} />
{renderInput()}
{renderShare()}
</React.Fragment>
);
function renderInput(): JSX.Element {
if (solveState !== SolveState.Unsolved) return (<></>);
return (
<div style={{ margin: '1em 0' }}>
<CrewPicker rules={rules} guesses={guesses} handleSelect={handleCrewSelect} />
</div>
);
}
function renderShare(): JSX.Element {
if (solveState === SolveState.Unsolved) return (<></>);
if (!props.gameTime) return (<></>);
const formatEvaluation = (evaluation: number) => {
if (evaluation === EvaluationState.Exact)
return '?';
else if (evaluation === EvaluationState.Adjacent)
return '?';
return '⬜';
};
const formatGrid = () => {
const shortId = `${props.gameTime.getUTCMonth()+1}/${props.gameTime.getUTCDate()}`;
let output = solveState === SolveState.Winner ? `I solved ${GAME_NAME} ${shortId} in ${guesses.length}!` : `${GAME_NAME} ${shortId} stumped me!`;
output += `\n${GAME_URL}`;
guessesEvaluated.forEach(guess => {
output += '\n';
['variant', 'series', 'rarity'].forEach(evaluation => {
output += formatEvaluation(guess.evaluation[evaluation]);
});
[0, 1, 2].forEach(idx => {
output += formatEvaluation(guess.evaluation.skills[idx]);
});
});
navigator.clipboard.writeText(output);
};
return (
<div style={{ marginTop: '2em' }}>
<Popup
content='Copied!'
on='click'
position='right center'
size='tiny'
trigger={
<Button icon='clipboard check' content='Copy results to clipboard' onClick={() => formatGrid()} />
}
/>
</div>
);
}
function handleCrewSelect(symbol: string): void {
if (symbol === '' || guesses.includes(symbol)) return;
guesses.push(symbol);
setGuesses([...guesses]);
if (guesses.includes(solution))
endGame(SolveState.Winner);
else if (guesses.length >= rules.guesses)
endGame(SolveState.Loser);
}
function endGame(solveState: number): void {
setSolveState(solveState);
if (props.onGameEnd) props.onGameEnd(solveState);
}
function getCrew(symbol: string): any {
const getSkillOrder = (base_skills: any) => {
const skills = Object.keys(base_skills).map(skill => {
return {
skill, core: base_skills[skill].core
};
}).sort((a, b) => b.core - a.core);
for (let i = skills.length; i < 3; i++) {
skills.push({ skill: '', core: 0 });
}
return skills;
};
const getVariantTraits = (traitsHidden: string[]) => {
// Get variant names from traits_hidden
const series = ['tos', 'tas', 'tng', 'ds9', 'voy', 'ent', 'dsc', 'pic', 'low', 'snw'];
const ignore = [
'female', 'male',
'artificial_life', 'nonhuman', 'organic', 'species_8472',
'admiral', 'captain', 'commander', 'lieutenant_commander', 'lieutenant', 'ensign', 'general', 'nagus', 'first_officer',
'ageofsail', 'bridge_crew', 'evsuit', 'gauntlet_jackpot', 'mirror', 'niners', 'original', 'crewman', 'yeoman',
'crew_max_rarity_5', 'crew_max_rarity_4', 'crew_max_rarity_3', 'crew_max_rarity_2', 'crew_max_rarity_1'
];
const variantTraits = [];
traitsHidden.forEach(trait => {
if (!series.includes(trait) && !ignore.includes(trait)) {
// Also ignore multishow variant traits, e.g. spock_tos, spock_dsc
if (!/_[a-z]{3}$/.test(trait) || !series.includes(trait.substr(-3)))
variantTraits.push(trait);
}
});
return variantTraits;
};
const getVariants = (variantTraits: string[], shortName: string) => {
const variants = variantTraits.slice();
// Dax hacks
const daxIndex = variants.indexOf('dax');
if (daxIndex >= 0) {
variantTraits.unshift(shortName);
variants[daxIndex] = shortName;
}
return variants;
};
const getUsableTraits = (crew: any, variantTraits: string[]) => {
const traits = variantTraits.slice();
['Female', 'Male'].forEach(usable => { if (crew.traits_hidden.includes(usable.toLowerCase())) traits.push(usable); });
const usableCollections = [
'A Little Stroll', 'Animated', 'Badda-Bing, Badda-Bang', 'Bride of Chaotica', 'Delphic Expanse',
'Holodeck Enthusiasts', 'Our Man Bashir', 'Pet People', 'Play Ball!', 'Set Sail!', 'Sherwood Forest',
'The Big Goodbye', 'The Wild West'
];
crew.collections.forEach(collection => {
if (usableCollections.includes(collection))
traits.push(collection);
});
return traits.concat(crew.traits_named);
};
const crew = portalCrew.find(crew => crew.symbol === symbol);
let shortName = crew.short_name;
// Dax hacks
if (shortName === 'E. Dax') shortName = 'Ezri';
if (shortName === 'J. Dax') shortName = 'Jadzia';
const variantTraits = getVariantTraits(crew.traits_hidden);
return {
symbol: crew.symbol,
name: crew.name,
short_name: shortName,
variants: getVariants(variantTraits, shortName),
imageUrlPortrait: crew.imageUrlPortrait ?? `${crew.portrait.file.substring(1).replaceAll('/', '_')}.png`,
flavor: crew.flavor,
series: crew.series ?? 'original',
rarity: crew.max_rarity,
skills: getSkillOrder(crew.base_skills),
traits: getUsableTraits(crew, variantTraits)
};
}
function evaluateGuess(crew: any): any {
const evaluateVariant = (symbol: string, variants: string[]) => {
if (solvedCrew.symbol === symbol)
return EvaluationState.Exact;
else {
let hasVariant = false;
solvedCrew.variants.forEach(solvedVariant => {
if (variants.includes(solvedVariant)) hasVariant = true;
});
if (hasVariant) return EvaluationState.Adjacent;
}
return EvaluationState.Wrong;
};
const evaluateSeries = (series: string) => {
const getEra = (series: string) => {
if (series === 'tos' || series === 'tas') return 1;
if (series === 'tng' || series === 'ds9' || series === 'voy' || series === 'ent') return 2;
if (series === 'original') return 0;
return 3;
};
if (solvedCrew.series === series)
return EvaluationState.Exact;
else if (getEra(solvedCrew.series) === getEra(series))
return EvaluationState.Adjacent;
return EvaluationState.Wrong;
};
const evaluateRarity = (rarity: number) => {
if (solvedCrew.rarity === rarity)
return EvaluationState.Exact;
else if (solvedCrew.rarity === rarity-1 || solvedCrew.rarity === rarity+1)
return EvaluationState.Adjacent;
return EvaluationState.Wrong;
};
const evaluateSkill = (skills: any[], index: number) => {
if (skills[index].skill === '') {
if (solvedCrew.skills[index].skill === '')
return EvaluationState.Exact;
}
else {
if (skills[index].skill === solvedCrew.skills[index].skill)
return EvaluationState.Exact;
else if (solvedCrew.skills.find(skill => skill.skill === skills[index].skill))
return EvaluationState.Adjacent;
}
return EvaluationState.Wrong;
};
const evaluateTraits = (traits: any[]) => {
const matches = [];
traits.forEach(trait => {
if (solvedCrew.traits.includes(trait) && !matches.includes(trait))
matches.push(trait);
});
return matches;
};
return {
crew: crew.symbol === solution ? EvaluationState.Exact : EvaluationState.Wrong,
variant: evaluateVariant(crew.symbol, crew.variants),
series: evaluateSeries(crew.series),
rarity: evaluateRarity(crew.rarity),
skills: [0, 1, 2].map(index => evaluateSkill(crew.skills, index)),
traits: evaluateTraits(crew.traits)
};
}
}
Example #20
Source File: App.tsx From watchparty with MIT License | 4 votes |
render() {
const sharer = this.state.participants.find((p) => p.isScreenShare);
const controls = (
<Controls
key={this.state.controlsTimestamp}
togglePlay={this.togglePlay}
onSeek={this.onSeek}
fullScreen={this.fullScreen}
toggleMute={this.toggleMute}
toggleSubtitle={this.toggleSubtitle}
setVolume={this.setVolume}
jumpToLeader={this.jumpToLeader}
paused={this.state.currentMediaPaused}
muted={this.isMuted()}
subtitled={this.isSubtitled()}
currentTime={this.getCurrentTime()}
duration={this.getDuration()}
disabled={!this.haveLock()}
leaderTime={this.isHttp() ? this.getLeaderTime() : undefined}
isPauseDisabled={this.isPauseDisabled()}
/>
);
const subscribeButton = (
<SubscribeButton
user={this.props.user}
isSubscriber={this.props.isSubscriber}
isCustomer={this.props.isCustomer}
/>
);
const displayRightContent =
this.state.showRightBar || this.state.fullScreen;
const rightBar = (
<Grid.Column
width={displayRightContent ? 4 : 1}
style={{ display: 'flex', flexDirection: 'column' }}
className={`${
this.state.fullScreen
? 'fullHeightColumnFullscreen'
: 'fullHeightColumn'
}`}
>
<Input
inverted
fluid
label={'My name is:'}
value={this.state.myName}
onChange={this.updateName}
style={{ visibility: displayRightContent ? '' : 'hidden' }}
icon={
<Icon
onClick={() => this.updateName(null, { value: generateName() })}
name="refresh"
inverted
circular
link
/>
}
/>
{
<Menu
inverted
widths={3}
style={{
marginTop: '4px',
marginBottom: '4px',
visibility: displayRightContent ? '' : 'hidden',
}}
>
<Menu.Item
name="chat"
active={this.state.currentTab === 'chat'}
onClick={() => {
this.setState({ currentTab: 'chat', unreadCount: 0 });
}}
as="a"
>
Chat
{this.state.unreadCount > 0 && (
<Label circular color="red">
{this.state.unreadCount}
</Label>
)}
</Menu.Item>
<Menu.Item
name="people"
active={this.state.currentTab === 'people'}
onClick={() => this.setState({ currentTab: 'people' })}
as="a"
>
People
<Label
circular
color={
getColorForString(
this.state.participants.length.toString()
) as SemanticCOLORS
}
>
{this.state.participants.length}
</Label>
</Menu.Item>
<Menu.Item
name="settings"
active={this.state.currentTab === 'settings'}
onClick={() => this.setState({ currentTab: 'settings' })}
as="a"
>
{/* <Icon name="setting" /> */}
Settings
</Menu.Item>
</Menu>
}
<Chat
chat={this.state.chat}
nameMap={this.state.nameMap}
pictureMap={this.state.pictureMap}
socket={this.socket}
scrollTimestamp={this.state.scrollTimestamp}
getMediaDisplayName={this.getMediaDisplayName}
hide={this.state.currentTab !== 'chat' || !displayRightContent}
isChatDisabled={this.state.isChatDisabled}
owner={this.state.owner}
user={this.props.user}
ref={this.chatRef}
/>
{this.state.state === 'connected' && (
<VideoChat
socket={this.socket}
participants={this.state.participants}
nameMap={this.state.nameMap}
pictureMap={this.state.pictureMap}
tsMap={this.state.tsMap}
rosterUpdateTS={this.state.rosterUpdateTS}
hide={this.state.currentTab !== 'people' || !displayRightContent}
owner={this.state.owner}
user={this.props.user}
/>
)}
<SettingsTab
hide={this.state.currentTab !== 'settings' || !displayRightContent}
user={this.props.user}
roomLock={this.state.roomLock}
setRoomLock={this.setRoomLock}
socket={this.socket}
isSubscriber={this.props.isSubscriber}
roomId={this.state.roomId}
isChatDisabled={this.state.isChatDisabled}
setIsChatDisabled={this.setIsChatDisabled}
owner={this.state.owner}
setOwner={this.setOwner}
vanity={this.state.vanity}
setVanity={this.setVanity}
roomLink={this.state.roomLink}
password={this.state.password}
setPassword={this.setPassword}
clearChat={this.clearChat}
roomTitle={this.state.roomTitle}
setRoomTitle={this.setRoomTitle}
roomDescription={this.state.roomDescription}
setRoomDescription={this.setRoomDescription}
roomTitleColor={this.state.roomTitleColor}
setRoomTitleColor={this.setRoomTitleColor}
mediaPath={this.state.mediaPath}
setMediaPath={this.setMediaPath}
/>
</Grid.Column>
);
return (
<React.Fragment>
{!this.state.isAutoPlayable && (
<Modal inverted basic open>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Button
primary
size="large"
onClick={() => {
this.setState({ isAutoPlayable: true });
this.setMute(false);
this.setVolume(1);
}}
icon
labelPosition="left"
>
<Icon name="volume up" />
Click to unmute
</Button>
</div>
</Modal>
)}
{this.state.multiStreamSelection && (
<MultiStreamModal
streams={this.state.multiStreamSelection}
setMedia={this.setMedia}
resetMultiSelect={this.resetMultiSelect}
/>
)}
{this.state.isVBrowserModalOpen && (
<VBrowserModal
isSubscriber={this.props.isSubscriber}
subscribeButton={subscribeButton}
closeModal={() => this.setState({ isVBrowserModalOpen: false })}
startVBrowser={this.startVBrowser}
user={this.props.user}
beta={this.props.beta}
/>
)}
{this.state.isScreenShareModalOpen && (
<ScreenShareModal
closeModal={() => this.setState({ isScreenShareModalOpen: false })}
startScreenShare={this.setupScreenShare}
/>
)}
{this.state.isFileShareModalOpen && (
<FileShareModal
closeModal={() => this.setState({ isFileShareModalOpen: false })}
startFileShare={this.setupFileShare}
/>
)}
{this.state.isSubtitleModalOpen && (
<SubtitleModal
closeModal={() => this.setState({ isSubtitleModalOpen: false })}
socket={this.socket}
currentSubtitle={this.state.currentSubtitle}
src={this.state.currentMedia}
haveLock={this.haveLock}
getMediaDisplayName={this.getMediaDisplayName}
beta={this.props.beta}
/>
)}
{this.state.error && <ErrorModal error={this.state.error} />}
{this.state.isErrorAuth && (
<PasswordModal
savedPasswords={this.state.savedPasswords}
roomId={this.state.roomId}
/>
)}
{this.state.errorMessage && (
<Message
negative
header="Error"
content={this.state.errorMessage}
style={{
position: 'fixed',
bottom: '10px',
right: '10px',
zIndex: 1000,
}}
></Message>
)}
{this.state.successMessage && (
<Message
positive
header="Success"
content={this.state.successMessage}
style={{
position: 'fixed',
bottom: '10px',
right: '10px',
zIndex: 1000,
}}
></Message>
)}
<TopBar
user={this.props.user}
isCustomer={this.props.isCustomer}
isSubscriber={this.props.isSubscriber}
roomTitle={this.state.roomTitle}
roomDescription={this.state.roomDescription}
roomTitleColor={this.state.roomTitleColor}
/>
{
<Grid stackable celled="internally">
<Grid.Row id="theaterContainer">
<Grid.Column
width={this.state.showRightBar ? 12 : 15}
className={
this.state.fullScreen
? 'fullHeightColumnFullscreen'
: 'fullHeightColumn'
}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
{!this.state.fullScreen && (
<React.Fragment>
<ComboBox
setMedia={this.setMedia}
playlistAdd={this.playlistAdd}
playlistDelete={this.playlistDelete}
playlistMove={this.playlistMove}
currentMedia={this.state.currentMedia}
getMediaDisplayName={this.getMediaDisplayName}
launchMultiSelect={this.launchMultiSelect}
streamPath={this.props.streamPath}
mediaPath={this.state.mediaPath}
disabled={!this.haveLock()}
playlist={this.state.playlist}
/>
<Separator />
<div
className="mobileStack"
style={{ display: 'flex', gap: '4px' }}
>
{this.screenShareStream && (
<Button
fluid
className="toolButton"
icon
labelPosition="left"
color="red"
onClick={this.stopScreenShare}
disabled={sharer?.id !== this.socket?.id}
>
<Icon name="cancel" />
Stop Share
</Button>
)}
{!this.screenShareStream &&
!sharer &&
!this.isVBrowser() && (
<Popup
content={`Share a tab or an application. Make sure to check "Share audio" for best results.`}
trigger={
<Button
fluid
className="toolButton"
disabled={!this.haveLock()}
icon
labelPosition="left"
color={'instagram'}
onClick={() => {
this.setState({
isScreenShareModalOpen: true,
});
}}
>
<Icon name={'slideshare'} />
Screenshare
</Button>
}
/>
)}
{!this.screenShareStream &&
!sharer &&
!this.isVBrowser() && (
<Popup
content="Launch a shared virtual browser"
trigger={
<Button
fluid
className="toolButton"
disabled={!this.haveLock()}
icon
labelPosition="left"
color="green"
onClick={() => {
this.setState({
isVBrowserModalOpen: true,
});
}}
>
<Icon name="desktop" />
VBrowser
</Button>
}
/>
)}
{this.isVBrowser() && (
<Popup
content="Choose the person controlling the VBrowser"
trigger={
<Dropdown
icon="keyboard"
labeled
className="icon"
style={{ height: '36px' }}
button
value={this.state.controller}
placeholder="No controller"
clearable
onChange={this.changeController}
selection
disabled={!this.haveLock()}
options={this.state.participants.map((p) => ({
text: this.state.nameMap[p.id] || p.id,
value: p.id,
}))}
></Dropdown>
}
/>
)}
{this.isVBrowser() && (
<Dropdown
icon="desktop"
labeled
className="icon"
style={{ height: '36px' }}
button
disabled={!this.haveLock()}
value={this.state.vBrowserResolution}
onChange={(_e, data) =>
this.setState({
vBrowserResolution: data.value as string,
})
}
selection
options={[
{
text: '1080p (Plus only)',
value: '1920x1080@30',
disabled: !this.state.isVBrowserLarge,
},
{
text: '720p',
value: '1280x720@30',
},
{
text: '576p',
value: '1024x576@60',
},
{
text: '486p',
value: '864x486@60',
},
{
text: '360p',
value: '640x360@60',
},
]}
></Dropdown>
)}
{this.isVBrowser() && (
<Button
fluid
className="toolButton"
icon
labelPosition="left"
color="red"
disabled={!this.haveLock()}
onClick={this.stopVBrowser}
>
<Icon name="cancel" />
Stop VBrowser
</Button>
)}
{!this.screenShareStream &&
!sharer &&
!this.isVBrowser() && (
<Popup
content="Stream your own video file"
trigger={
<Button
fluid
className="toolButton"
disabled={!this.haveLock()}
icon
labelPosition="left"
onClick={() => {
this.setState({
isFileShareModalOpen: true,
});
}}
>
<Icon name="file" />
File
</Button>
}
/>
)}
{false && (
<SearchComponent
setMedia={this.setMedia}
playlistAdd={this.playlistAdd}
type={'youtube'}
streamPath={this.props.streamPath}
disabled={!this.haveLock()}
/>
)}
{Boolean(this.props.streamPath) && (
<SearchComponent
setMedia={this.setMedia}
playlistAdd={this.playlistAdd}
type={'stream'}
streamPath={this.props.streamPath}
launchMultiSelect={this.launchMultiSelect}
disabled={!this.haveLock()}
/>
)}
</div>
<Separator />
</React.Fragment>
)}
<div style={{ flexGrow: 1 }}>
<div id="playerContainer">
{(this.state.loading ||
!this.state.currentMedia ||
this.state.nonPlayableMedia) && (
<div
id="loader"
className="videoContent"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{this.state.loading && (
<Dimmer active>
<Loader>
{this.isVBrowser()
? 'Launching virtual browser. This can take up to a minute.'
: ''}
</Loader>
</Dimmer>
)}
{!this.state.loading && !this.state.currentMedia && (
<Message
color="yellow"
icon="hand point up"
header="You're not watching anything!"
content="Pick something to watch above."
/>
)}
{!this.state.loading &&
this.state.nonPlayableMedia && (
<Message
color="red"
icon="frown"
header="It doesn't look like this is a media file!"
content="Maybe you meant to launch a VBrowser if you're trying to visit a web page?"
/>
)}
</div>
)}
<iframe
style={{
display:
this.isYouTube() && !this.state.loading
? 'block'
: 'none',
}}
title="YouTube"
id="leftYt"
className="videoContent"
allowFullScreen
frameBorder="0"
allow="autoplay"
src="https://www.youtube.com/embed/?enablejsapi=1&controls=0&rel=0"
/>
{this.isVBrowser() &&
this.getVBrowserPass() &&
this.getVBrowserHost() ? (
<VBrowser
username={this.socket.id}
password={this.getVBrowserPass()}
hostname={this.getVBrowserHost()}
controlling={this.state.controller === this.socket.id}
setLoadingFalse={this.setLoadingFalse}
resolution={this.state.vBrowserResolution}
doPlay={this.doPlay}
setResolution={(data: string) =>
this.setState({ vBrowserResolution: data })
}
/>
) : (
<video
style={{
display:
(this.isVideo() && !this.state.loading) ||
this.state.fullScreen
? 'block'
: 'none',
width: '100%',
maxHeight:
'calc(100vh - 62px - 36px - 36px - 8px - 41px - 16px)',
}}
id="leftVideo"
onEnded={this.onVideoEnded}
playsInline
></video>
)}
</div>
</div>
{this.state.currentMedia && controls}
{Boolean(this.state.total) && (
<div>
<Progress
size="tiny"
color="green"
inverted
value={this.state.downloaded}
total={this.state.total}
// indicating
label={
Math.min(
(this.state.downloaded / this.state.total) * 100,
100
).toFixed(2) +
'% - ' +
formatSpeed(this.state.speed) +
' - ' +
this.state.connections +
' connections'
}
></Progress>
</div>
)}
</div>
<Button
style={{
position: 'absolute',
top: '50%',
right: 'calc(0% - 18px)',
zIndex: 900,
}}
circular
size="mini"
icon={this.state.showRightBar ? 'angle right' : 'angle left'}
onClick={() =>
this.setState({ showRightBar: !this.state.showRightBar })
}
/>
</Grid.Column>
{rightBar}
</Grid.Row>
</Grid>
}
</React.Fragment>
);
}
Example #21
Source File: voyagecalculator.tsx From website with MIT License | 4 votes |
VoyageInput = (props: VoyageInputProps) => {
const { voyageConfig, myCrew, allShips, useInVoyage } = props;
const [bestShip, setBestShip] = React.useState(undefined);
const [consideredCrew, setConsideredCrew] = React.useState([]);
const [calculator, setCalculator] = React.useState(isMobile ? 'ussjohnjay' : 'iampicard');
const [calcOptions, setCalcOptions] = React.useState({});
const [telemetryOptOut, setTelemetryOptOut] = useStateWithStorage('telemetryOptOut', false, { rememberForever: true });
const [requests, setRequests] = React.useState([]);
const [results, setResults] = React.useState([]);
React.useEffect(() => {
// Note that allShips is missing the default ship for some reason (1* Constellation Class)
// This WILL break voyagecalculator if that's the only ship a player owns
const consideredShips = [];
allShips.filter(ship => ship.owned).forEach(ship => {
const traited = ship.traits.find(trait => trait === voyageConfig.ship_trait);
let entry = {
ship: ship,
score: ship.antimatter + (traited ? 150 : 0),
traited: traited,
bestIndex: Math.min(ship.index.left, ship.index.right)
};
consideredShips.push(entry);
});
consideredShips.sort((a, b) => {
if (a.score === b.score) return a.bestIndex - b.bestIndex;
return b.score - a.score;
});
setBestShip(consideredShips[0]);
setRequests([]);
setResults([]);
}, [voyageConfig]);
React.useEffect(() => {
return function cleanup() {
// Cancel active calculations when leaving page
requests.forEach(request => {
if (request.calcState == CalculatorState.InProgress)
request.abort();
});
}
}, []);
// Scroll here when calculator started, finished
const topAnchor = React.useRef(null);
const calculators = CALCULATORS.helpers.map(helper => {
return { key: helper.id, value: helper.id, text: helper.name };
});
calculators.push({ key: 'all', value: 'all', text: 'All calculators (slower)' });
return (
<React.Fragment>
<div ref={topAnchor} />
{renderBestShip()}
{renderResults()}
{requests.length > 0 && <Header as='h3'>Options</Header>}
<Form>
<InputCrewOptions myCrew={myCrew} updateConsideredCrew={setConsideredCrew} />
<Form.Group inline>
<Form.Field
control={Select}
label='Calculator'
options={calculators}
value={calculator}
onChange={(e, { value }) => setCalculator(value)}
placeholder='Select calculator'
/>
{CALCULATORS.fields.filter(field => field.calculators.includes(calculator) || calculator == 'all').map(field => (
<Form.Field
key={field.id}
control={Select} /* Only control allowed at the moment */
label={field.name}
options={field.options}
value={calcOptions[field.id] ?? field.default}
placeholder={field.description}
onChange={(e, { value }) => setCalcOptions(prevOptions =>
{
const newValue = { [field.id]: value };
return {...prevOptions, ...newValue};
}
)}
/>
))}
</Form.Group>
<Form.Group>
<Form.Button primary onClick={() => startCalculation()}>
Calculate best crew selection
</Form.Button>
{voyageConfig.state &&
<Form.Button onClick={()=> useInVoyage()}>
Return to in voyage calculator
</Form.Button>
}
</Form.Group>
</Form>
<Message style={{ marginTop: '2em' }}>
<Message.Content>
<Message.Header>Privacy Notice</Message.Header>
<p>We use anonymous statistics aggregated from voyage calculations to improve DataCore and power our <b><Link to='/hall_of_fame'>Voyage Hall of Fame</Link></b>.</p>
<Form>
<Form.Field
control={Checkbox}
label={<label>Permit DataCore to collect anonymous voyage stats</label>}
checked={!telemetryOptOut}
onChange={(e, { checked }) => setTelemetryOptOut(!checked) }
/>
</Form>
</Message.Content>
</Message>
</React.Fragment>
);
function renderBestShip(): JSX.Element {
if (!bestShip) return (<></>);
const direction = bestShip.ship.index.right < bestShip.ship.index.left ? 'right' : 'left';
const index = bestShip.ship.index[direction] ?? 0;
return (
<Card fluid>
<Card.Content>
<Image floated='left' src={`${process.env.GATSBY_ASSETS_URL}${bestShip.ship.icon.file.substr(1).replace('/', '_')}.png`} style={{ height: '4em' }} />
<Card.Header>{bestShip.ship.name}</Card.Header>
<p>best ship{bestShip.traited && (<span style={{ marginLeft: '1em' }}>{` +`}{allTraits.ship_trait_names[voyageConfig.ship_trait]}</span>)}</p>
<p style={{ marginTop: '.5em' }}>Tap <Icon name={`arrow ${direction}`} />{index} time{index != 1 ? 's' : ''} on your voyage ship selection screen to select {bestShip.ship.name}.</p>
</Card.Content>
</Card>
);
}
function renderResults(): JSX.Element {
if (results.length == 0)
return (<></>);
const showPopup = (result) => <Popup basic content={<p>{result.result.postscript}</p>} trigger={<p>{result.name}</p>} />
const panes = results.map(result => ({
menuItem: { key: result.id, content: result.result ? showPopup(result) : result.name },
render: () => (
<VoyageResultPane result={result.result}
requests={requests} requestId={result.requestId}
calcState={result.calcState} abortCalculation={abortCalculation}
/>
)
}));
return (
<React.Fragment>
<Header as='h3'>Recommended Lineups</Header>
<Tab menu={{ pointing: true }}
panes={panes}
/>
</React.Fragment>
);
}
function scrollToAnchor(): void {
if (!topAnchor.current) return;
topAnchor.current.scrollIntoView({
behavior: 'smooth'
}, 500);
}
function startCalculation(): void {
const helperConfig = {
voyageConfig, bestShip, consideredCrew, calcOptions,
resultsCallback: handleResults,
isMobile
};
CALCULATORS.helpers.forEach(helper => {
if (helper.id == calculator || calculator == 'all') {
const request = helper.helper(helperConfig);
requests.push(request);
results.push({
id: request.id,
requestId: request.id,
name: 'Calculating...',
calcState: CalculatorState.InProgress
});
request.start();
}
});
setRequests([...requests]);
setResults([...results]);
scrollToAnchor();
}
function handleResults(requestId: string, reqResults: any[], calcState: number): void {
reqResults.forEach((reqResult, idx) => {
// Update existing pane with results
if (idx == 0) {
setResults(prevResults => {
const result = prevResults.find(r => r.id == requestId);
if (calcState == CalculatorState.Done) {
result.name = formatTime(reqResult.estimate.refills[0].result);
result.calcState = CalculatorState.Done;
sendTelemetry(requestId, reqResult);
}
result.result = reqResult;
return [...prevResults];
});
}
// Add new panes if multiple results generated by this request
else {
setResults(prevResults => [...prevResults, {
id: requestId+'-'+idx,
requestId,
name: formatTime(reqResult.estimate.refills[0].result),
calcState: CalculatorState.Done,
result: reqResult
}]);
}
});
if (calcState == CalculatorState.Done) scrollToAnchor();
}
function abortCalculation(requestId: string): void {
const request = requests.find(r => r.id == requestId);
if (request) {
request.abort();
setResults(prevResults => {
const result = prevResults.find(prev => prev.id == requestId);
if (result.result) {
result.name = formatTime(result.result.estimate.refills[0].result);
result.calcState = CalculatorState.Done;
}
else {
const index = prevResults.findIndex(prev => prev.id == requestId);
prevResults.splice(index, 1);
}
return [...prevResults];
});
}
}
function sendTelemetry(requestId: string, result: any): void {
if (telemetryOptOut) return;
const request = requests.find(r => r.id == requestId);
const estimatedDuration = result.estimate.refills[0].result*60*60;
try {
fetch(`${process.env.GATSBY_DATACORE_URL}api/telemetry`, {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'voyageCalc',
data: {
voyagers: result.entries.map((entry) => entry.choice.symbol),
estimatedDuration,
calculator: request ? request.calculator : ''
}
})
});
}
catch (err) {
console.log('An error occurred while sending telemetry', err);
}
}
}
Example #22
Source File: Chat.tsx From watchparty with MIT License | 4 votes |
ChatMessage = ({
message,
nameMap,
pictureMap,
formatMessage,
user,
socket,
owner,
isChatDisabled,
setReactionMenu,
handleReactionClick,
className,
}: {
message: ChatMessage;
nameMap: StringDict;
pictureMap: StringDict;
formatMessage: (cmd: string, msg: string) => React.ReactNode;
user: firebase.User | undefined;
socket: Socket;
owner: string | undefined;
isChatDisabled: boolean | undefined;
setReactionMenu: Function;
handleReactionClick: Function;
className: string;
}) => {
const { id, timestamp, cmd, msg, system, isSub, reactions } = message;
const spellFull = 5; // the number of people whose names should be written out in full in the reaction popup
return (
<Comment className={`${classes.comment} ${className}`}>
{id ? (
<Popup
content="WatchParty Plus subscriber"
disabled={!isSub}
trigger={
<Comment.Avatar
className={isSub ? classes.subscriber : ''}
src={
pictureMap[id] ||
getDefaultPicture(nameMap[id], getColorForStringHex(id))
}
/>
}
/>
) : null}
<Comment.Content>
<UserMenu
displayName={nameMap[id] || id}
user={user}
timestamp={timestamp}
socket={socket}
userToManage={id}
isChatMessage
disabled={!Boolean(owner && owner === user?.uid)}
trigger={
<Comment.Author as="a" className="light">
{Boolean(system) && 'System'}
{nameMap[id] || id}
</Comment.Author>
}
/>
<Comment.Metadata className="dark">
<div>{new Date(timestamp).toLocaleTimeString()}</div>
</Comment.Metadata>
<Comment.Text className="light system">
{cmd && formatMessage(cmd, msg)}
</Comment.Text>
<Linkify
componentDecorator={(
decoratedHref: string,
decoratedText: string,
key: string
) => (
<SecureLink href={decoratedHref} key={key}>
{decoratedText}
</SecureLink>
)}
>
<Comment.Text className="light">{!cmd && msg}</Comment.Text>
</Linkify>
<div className={classes.commentMenu}>
<Icon
onClick={(e: MouseEvent) => {
const viewportOffset = (e.target as any).getBoundingClientRect();
setReactionMenu(
true,
id,
timestamp,
viewportOffset.top,
viewportOffset.right
);
}}
name={'' as any}
inverted
link
disabled={isChatDisabled}
style={{
opacity: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: 10,
margin: 0,
}}
>
<span
role="img"
aria-label="React"
style={{ margin: 0, fontSize: 18 }}
>
?
</span>
</Icon>
</div>
<TransitionGroup>
{Object.keys(reactions ?? []).map((key) =>
reactions?.[key].length ? (
<CSSTransition
key={key}
timeout={200}
classNames={{
enter: classes['reaction-enter'],
enterActive: classes['reaction-enter-active'],
exit: classes['reaction-exit'],
exitActive: classes['reaction-exit-active'],
}}
unmountOnExit
>
<Popup
content={`${reactions[key]
.slice(0, spellFull)
.map((id) => nameMap[id] || 'Unknown')
.concat(
reactions[key].length > spellFull
? [`${reactions[key].length - spellFull} more`]
: []
)
.reduce(
(text, value, i, array) =>
text + (i < array.length - 1 ? ', ' : ' and ') + value
)} reacted.`}
offset={[0, 6]}
trigger={
<div
className={`${classes.reactionContainer} ${
reactions[key].includes(socket.id)
? classes.highlighted
: ''
}`}
onClick={() =>
handleReactionClick(key, message.id, message.timestamp)
}
>
<span
style={{
fontSize: 17,
position: 'relative',
bottom: 1,
}}
>
{key}
</span>
<SwitchTransition>
<CSSTransition
key={key + '-' + reactions[key].length}
classNames={{
enter: classes['reactionCounter-enter'],
enterActive:
classes['reactionCounter-enter-active'],
exit: classes['reactionCounter-exit'],
exitActive: classes['reactionCounter-exit-active'],
}}
addEndListener={(node, done) =>
node.addEventListener('transitionend', done, false)
}
unmountOnExit
>
<span
className={classes.reactionCounter}
style={{
color: 'rgba(255, 255, 255, 0.85)',
marginLeft: 3,
}}
>
{reactions[key].length}
</span>
</CSSTransition>
</SwitchTransition>
</div>
}
/>
</CSSTransition>
) : null
)}
</TransitionGroup>
</Comment.Content>
</Comment>
);
}
Example #23
Source File: vaultcrew.tsx From website with MIT License | 4 votes |
render() {
const { crew, itemsReady } = this.props;
const SZ = (scale: number) => (this.props.size * scale).toFixed(2);
let borderColor = new TinyColor(CONFIG.RARITIES[crew.max_rarity].color);
let star_reward = `${process.env.GATSBY_ASSETS_URL}atlas/star_reward.png`;
let star_reward_inactive = `${process.env.GATSBY_ASSETS_URL}atlas/star_reward_inactive.png`;
let iconStyle: React.CSSProperties = {
display: 'inline-block',
height: SZ(2.4) + 'em',
paddingTop: SZ(0.4) + 'em',
paddingRight: SZ(0.4) + 'em'
};
let rarity = [];
for (let i = 0; i < crew.rarity; i++) {
rarity.push(<img key={i} src={star_reward} style={iconStyle} />);
}
for (let i = crew.rarity; i < crew.max_rarity; i++) {
rarity.push(<img key={i} src={star_reward_inactive} style={iconStyle} />);
}
let skillicons = [];
let skills_sorted = Object.entries(crew.base_skills)
.sort((a, b) => { a[1].core - b[1].core });
skills_sorted.forEach((s) => {
let skillName = s[0];
skillicons.push(<img key={skillName} src={`${process.env.GATSBY_ASSETS_URL}atlas/icon_${skillName}.png`} style={iconStyle} />);
});
let divStyle: React.CSSProperties = this.props.style || {};
divStyle.display = 'grid';
divStyle.width = SZ(22) + 'em';
divStyle.height = SZ(20) + 'em';
divStyle.gridTemplateColumns = `${SZ(16)}em ${SZ(6)}em`;
divStyle.gridTemplateRows = `${SZ(16)}em ${SZ(4)}em`;
divStyle.gridTemplateAreas = "'portrait equipment' 'footer footer'";
divStyle.borderWidth = SZ(0.2) + 'em';
divStyle.borderRadius = SZ(0.2) + 'em';
divStyle.borderStyle = 'solid';
divStyle.borderColor = borderColor.toHexString();
divStyle.backgroundColor = borderColor
.clone()
.darken(50)
.toHexString();
let equipmentColumnStyle: React.CSSProperties = {
gridArea: 'equipment',
display: 'grid',
textAlign: 'center',
gridTemplateRows: 'repeat(4, 1fr)',
margin: SZ(0.2) + 'em',
gap: SZ(0.1) + 'em'
};
let equipmentCellImg: React.CSSProperties = {
height: SZ(3) + 'em',
borderWidth: SZ(0.2) + 'em',
borderStyle: 'solid',
borderColor: borderColor.toHexString(),
borderRadius: SZ(0.4) + 'em'
};
let cardFooter: React.CSSProperties = {
gridArea: 'footer',
display: 'grid',
gridTemplateColumns: '1fr 1fr',
backgroundColor: borderColor
.clone()
.darken(40)
.toHexString(),
padding: SZ(0.4) + 'em',
width: '98%',
height: '90%'
};
let cardFooterSkills: React.CSSProperties = {
justifySelf: 'start',
backgroundColor: borderColor
.clone()
.darken(50)
.toHexString(),
padding: SZ(0.1) + 'em'
};
let cardFooterLevel: React.CSSProperties = {
justifySelf: 'end',
backgroundColor: borderColor
.clone()
.darken(50)
.toHexString(),
padding: SZ(0.1) + 'em',
fontSize: SZ(2.2) + 'em',
color: 'white',
display: 'flex'
};
// Dec levels can be either end of one equip range or start of the next (e.g. lvl 20 is 10-20 or 20-30)
// Assume at the start of next range unless has multiple equips
let startlevel = Math.floor(crew.level / 10) * 4;
if (crew.level % 10 == 0 && crew.equipment.length > 1) startlevel = startlevel - 4;
let eqimgs = [];
if (!crew.equipment_slots[startlevel] || !itemsReady) {
//console.error(`Missing equipment slots information for crew '${crew.name}'`);
//console.log(crew);
eqimgs = [
'items_equipment_box02_icon.png',
'items_equipment_box02_icon.png',
'items_equipment_box02_icon.png',
'items_equipment_box02_icon.png'
];
} else {
eqimgs = [
crew.equipment_slots[startlevel].imageUrl,
crew.equipment_slots[startlevel + 1].imageUrl,
crew.equipment_slots[startlevel + 2].imageUrl,
crew.equipment_slots[startlevel + 3].imageUrl
];
}
if (crew.equipment) {
[0, 1, 2, 3].forEach(idx => {
if (crew.equipment.indexOf(idx) < 0) {
eqimgs[idx] = 'items_equipment_box02_icon.png';
}
});
}
let portraitDivStyle: React.CSSProperties = {
gridArea: 'portrait',
position: 'relative'
};
if (crew.immortal > 0 || (crew.rarity === crew.max_rarity && crew.level === 100 && crew.equipment.length === 4)) {
// For immortalized crew only
portraitDivStyle.backgroundSize = 'cover';
portraitDivStyle.backgroundImage = `url(${process.env.GATSBY_ASSETS_URL}collection_vault_vault_item_bg_immortalized_256.png)`;
}
return (
<div style={divStyle}>
<div style={portraitDivStyle}>
<Popup
on="click"
header={crew.name}
content={formatCrewStats(crew)}
trigger={<img src={`${process.env.GATSBY_ASSETS_URL}${crew.imageUrlPortrait}`} style={{ width: '100%' }} />}
/>
<div
style={{
position: 'absolute',
bottom: '0px',
width: '100%',
textAlign: 'initial',
backgroundColor: 'rgba(0, 0, 0, 0.5)'
}}
>
{rarity}
</div>
{crew.immortal > 0 && (
<div
style={{
position: 'absolute',
top: '0px',
width: '100%',
textAlign: 'initial',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
fontSize: SZ(1.2) + 'em'
}}
>
Frozen: {crew.immortal} in vault
</div>
)}
</div>
<div style={equipmentColumnStyle}>
<div style={{ display: 'inline-block' }}>
<img style={equipmentCellImg} src={`${process.env.GATSBY_ASSETS_URL}${eqimgs[0]}`} />
</div>
<div style={{ display: 'inline-block' }}>
<img style={equipmentCellImg} src={`${process.env.GATSBY_ASSETS_URL}${eqimgs[1]}`} />
</div>
<div style={{ display: 'inline-block' }}>
<img style={equipmentCellImg} src={`${process.env.GATSBY_ASSETS_URL}${eqimgs[2]}`} />
</div>
<div style={{ display: 'inline-block' }}>
<img style={equipmentCellImg} src={`${process.env.GATSBY_ASSETS_URL}${eqimgs[3]}`} />
</div>
</div>
<div style={cardFooter}>
<div style={cardFooterSkills}>
<span>{skillicons}</span>
</div>
<div style={cardFooterLevel}>
<span style={{ margin: 'auto' }}>{crew.level}</span>
</div>
</div>
</div>
);
}
Example #24
Source File: Controls.tsx From watchparty with MIT License | 4 votes |
render() {
const {
togglePlay,
onSeek,
fullScreen,
toggleMute,
toggleSubtitle,
jumpToLeader,
currentTime,
duration,
leaderTime,
isPauseDisabled,
disabled,
subtitled,
paused,
} = this.props;
const { muted, volume } = this.state;
const isBehind = leaderTime && leaderTime - currentTime > 5;
return (
<div className="controls">
<Icon
size="large"
onClick={() => {
togglePlay();
}}
className="control action"
disabled={disabled || isPauseDisabled}
name={paused ? 'play' : 'pause'}
/>
<Popup
content={
(isBehind ? "We've detected that your stream is behind. " : '') +
'Click to sync to leader.'
}
trigger={
<Icon
size="large"
onClick={jumpToLeader}
className={`control action ${isBehind ? 'glowing' : ''}`}
name={'angle double right'}
/>
}
/>
<div className="control">{formatTimestamp(currentTime)}</div>
<Progress
size="tiny"
color="blue"
onClick={
duration < Infinity && !this.props.disabled ? onSeek : undefined
}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
onMouseMove={this.onMouseMove}
className="control action"
inverted
style={{
flexGrow: 1,
marginTop: 0,
marginBottom: 0,
position: 'relative',
minWidth: '50px',
}}
value={currentTime}
total={duration}
>
{duration < Infinity && this.state.showTimestamp && (
<div
style={{
position: 'absolute',
bottom: '0px',
left: `calc(${this.state.posTimestamp * 100 + '% - 27px'})`,
pointerEvents: 'none',
}}
>
<Label basic color="blue" pointing="below">
<div style={{ width: '34px' }}>
{formatTimestamp(this.state.currTimestamp)}
</div>
</Label>
</div>
)}
</Progress>
<div className="control">{formatTimestamp(duration)}</div>
<Icon
size="large"
onClick={() => {
toggleSubtitle();
}}
className="control action"
name={subtitled ? 'closed captioning' : 'closed captioning outline'}
title="Captions"
/>
<Icon
size="large"
onClick={() => fullScreen(false)}
className="control action"
style={{ transform: 'rotate(90deg)' }}
name="window maximize outline"
title="Theater Mode"
/>
<Icon
size="large"
onClick={() => fullScreen(true)}
className="control action"
name="expand"
title="Fullscreen"
/>
<Icon
size="large"
onClick={() => {
toggleMute();
this.setState({ muted: !this.state.muted });
}}
className="control action"
name={muted ? 'volume off' : 'volume up'}
title="Mute"
/>
<div style={{ width: '100px', marginRight: '10px' }}>
<Slider
value={volume}
color="blue"
settings={{
min: 0,
max: 1,
step: 0.01,
onChange: (value: number) => {
this.setState({ volume: value });
this.props.setVolume(value);
},
}}
/>
</div>
</div>
);
}
Example #25
Source File: assignmentslist.tsx From website with MIT License | 4 votes |
AssignmentsList = (props: AssignmentsList) => {
const { shuttlers, setShuttlers, assigned, setAssigned, crewScores, updateCrewScores } = props;
const [shuttleScores, setShuttleScores] = React.useState([]);
const [editAssignment, setEditAssignment] = React.useState(undefined);
const [scoreLoadQueue, setScoreLoadQueue] = React.useState('');
React.useEffect(() => {
updateShuttleScores();
}, [assigned]);
const myCrew = props.crew;
const SeatAssignmentRow = (props: { shuttleId: string, seatNum: number, seat: ShuttleSeat }) => {
const { shuttleId, seatNum, seat } = props;
let assignedCrew;
const seated = assigned.find(seat => seat.shuttleId === shuttleId && seat.seatNum === seatNum);
if (seated) {
assignedCrew = myCrew.find(crew => crew.id === seated.assignedId && crew.symbol === seated.assignedSymbol);
if (!assignedCrew) assignedCrew = myCrew.find(crew => crew.symbol === seated.assignedSymbol);
}
const lockAttributes = {};
if (seated?.locked) lockAttributes.color = 'yellow';
return (
<Table.Row key={seatNum} style={{ cursor: 'pointer' }}
onClick={() => { if (seat.skillA) setEditAssignment({shuttleId, seatNum}); }}
>
<Table.Cell textAlign='center'>
<SeatSkillView seat={seat} />
</Table.Cell>
<Table.Cell textAlign={assignedCrew ? 'left' : 'right'}>
{assignedCrew && (<SeatCrewView crew={assignedCrew} />)}
{!assignedCrew && (<span style={{ color: 'gray' }}>(Open seat)</span>)}
</Table.Cell>
<Table.Cell>
{assignedCrew?.immortal > 0 && (<Icon name='snowflake' />)}
{assignedCrew?.prospect && (<Icon name='add user' />)}
</Table.Cell>
<Table.Cell textAlign='center'>
{assignedCrew && (
<Button.Group>
<Button compact icon='lock' {... lockAttributes}
onClick={(e) => { toggleAssignmentLock(shuttleId, seatNum); e.stopPropagation(); }} />
<Button compact icon='x'
onClick={(e) => { updateAssignment(shuttleId, seatNum); e.stopPropagation(); }} />
</Button.Group>
)}
</Table.Cell>
</Table.Row>
);
};
const SeatAssignmentPicker = () => {
const { shuttleId, seatNum } = editAssignment;
const [paginationPage, setPaginationPage] = React.useState(1);
const seat = shuttlers.shuttles.find(shuttle => shuttle.id === shuttleId).seats[seatNum];
const ssId = getSkillSetId(seat);
const scores = crewScores.skillsets[ssId];
if (!scores) {
if (scoreLoadQueue === '') {
setScoreLoadQueue(ssId);
updateCrewScores([seat], () => setScoreLoadQueue(''));
}
return (<></>);
}
const shuttle = shuttlers.shuttles.find(shuttle => shuttle.id === shuttleId);
return (
<Modal
open={true}
onClose={() => setEditAssignment(undefined)}
>
<Modal.Header>
{shuttle.name}
{shuttleScores[shuttleId] ?
<span style={{ fontSize: '.95em', fontWeight: 'normal', paddingLeft: '1em' }}>
({(shuttleScores[shuttleId].chance*100).toFixed(1)}% Chance)
</span>
: ''}
</Modal.Header>
<Modal.Content scrolling>
{scores && renderTable()}
</Modal.Content>
<Modal.Actions>
<Button icon='forward' content='Next Seat' onClick={() => cycleShuttleSeat()} />
<Button positive onClick={() => setEditAssignment(undefined)}>
Close
</Button>
</Modal.Actions>
</Modal>
);
function renderTable(): JSX.Element {
let assignedCrew;
const seated = assigned.find(seat => seat.shuttleId === shuttleId && seat.seatNum == seatNum);
if (seated) {
assignedCrew = myCrew.find(crew => crew.id === seated.assignedId && crew.symbol === seated.assignedSymbol);
if (!assignedCrew) assignedCrew = myCrew.find(crew => crew.symbol === seated.assignedSymbol);
}
// Pagination
const rowsPerPage = 10;
const totalPages = Math.ceil(scores.length / rowsPerPage);
const data = scores.slice(rowsPerPage * (paginationPage - 1), rowsPerPage * paginationPage).map(score => {
const scoreCrew = myCrew.find(crew => crew.id === score.id);
return {...scoreCrew, ...score}
});
return (
<React.Fragment>
<Table striped selectable singleLine compact='very'>
<Table.Header>
<Table.Row>
<Table.HeaderCell />
<Table.HeaderCell colSpan={2}>Best <span style={{ padding: '0 .5em' }}><SeatSkillView seat={seat} /></span> Crew</Table.HeaderCell>
<Table.HeaderCell textAlign='center'>Here<Popup trigger={<Icon name='help' />} content='Using this crew here will result in this net change to the success chance of this shuttle' /></Table.HeaderCell>
<Table.HeaderCell>Current Assignment</Table.HeaderCell>
<Table.HeaderCell textAlign='center'>There<Popup trigger={<Icon name='help' />} content='Removing this crew from their current assignment will leave an open seat on that shuttle, resulting in this success chance' /></Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{data.map((crew, idx) => renderRow(crew, idx, assignedCrew))}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan={6}>
<Pagination
totalPages={totalPages}
activePage={paginationPage}
onPageChange={(e, { activePage }) => setPaginationPage(activePage)}
/>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</React.Fragment>
);
}
function renderRow(crew: any, idx: number, assignedCrew: any): JSX.Element {
const currentSeat = assigned.find(seat => seat.assignedId === crew.id && seat.assignedSymbol === crew.symbol);
const currentShuttle = currentSeat ? shuttlers.shuttles.find(shuttle => shuttle.id === currentSeat.shuttleId) : undefined;
return (
<Table.Row key={idx} style={{ cursor: 'pointer' }}
onClick={() => {
if (!assignedCrew || crew.id !== assignedCrew.id)
updateAssignment(shuttleId, seatNum, crew, true);
setEditAssignment(undefined);
}}
>
<Table.Cell textAlign='center'>
{assignedCrew?.id === crew.id && (<Icon color='green' name='check' />)}
</Table.Cell>
<Table.Cell><SeatCrewView crew={crew} /></Table.Cell>
<Table.Cell>
{crew.immortal > 0 && (<Icon name='snowflake' />)}
{crew.prospect && (<Icon name='add user' />)}
</Table.Cell>
<Table.Cell textAlign='center'>
{renderScoreChange(shuttleId, seatNum, crew.score)}
</Table.Cell>
<Table.Cell>
{currentShuttle?.name}
{currentShuttle?.id === shuttleId && <span style={{ paddingLeft: '1em' }}><i>(This Shuttle)</i></span>}
{currentSeat?.locked && <span style={{ paddingLeft: '1em' }}><Icon name='lock' /></span>}
</Table.Cell>
<Table.Cell textAlign='center'>
{currentSeat && renderScoreChange(currentSeat.shuttleId, currentSeat.seatNum, 0)}
</Table.Cell>
</Table.Row>
);
}
function renderScoreChange(shuttleId: string, seatNum: number, replacementScore: number = 0): JSX.Element {
if (!shuttleScores[shuttleId]) return (<></>);
const newScores = [...shuttleScores[shuttleId].scores];
newScores[seatNum] = replacementScore;
const DIFFICULTY = 2000;
const dAvgSkill = newScores.reduce((a, b) => (a + b), 0)/newScores.length;
const dChance = 1/(1+Math.pow(Math.E, 3.5*(0.5-dAvgSkill/DIFFICULTY)));
const attributes = {};
if (replacementScore === 0) {
if (dChance*100 >= 90) attributes.style = { color: 'green', fontWeight: 'bold' };
return (<span {...attributes}>{Math.floor(dChance*100)}%</span>);
}
const dDelta = dChance - shuttleScores[shuttleId].chance;
if (dDelta > 0 && dChance*100 >= 90)
attributes.style = { color: 'green', fontWeight: 'bold' };
return (<span {...attributes}>{dDelta > 0 ? '+' : ''}{(dDelta*100).toFixed(1)}%</span>);
}
function cycleShuttleSeat(): void {
const nextAssignment = {
shuttleId: shuttleId,
seatNum: seatNum + 1 >= shuttle.seats.length ? 0 : seatNum + 1
};
setEditAssignment(nextAssignment);
}
};
const data = shuttlers.shuttles.slice()
.filter(shuttle => shuttle.groupId === props.groupId && shuttle.priority > 0)
.sort((a, b) => a.priority - b.priority);
return (
<React.Fragment>
<p>You can rearrange crew to balance shuttle chances as you see fit. Click a seat to change the crew assigned to it. Lock an assignment to keep that crew in that seat when requesting new recommendations.</p>
<Table celled striped compact='very'>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Mission</Table.HeaderCell>
<Table.HeaderCell textAlign='center'>Faction</Table.HeaderCell>
<Table.HeaderCell textAlign='center'>Seat Assignments</Table.HeaderCell>
<Table.HeaderCell textAlign='center'>Success Chance</Table.HeaderCell>
<Table.HeaderCell />
</Table.Row>
</Table.Header>
<Table.Body>
{data.length === 0 && (
<Table.Row>
<Table.Cell colSpan={6} textAlign='center'>
No missions selected.
</Table.Cell>
</Table.Row>
)}
{data.map(shuttle => (
<Table.Row key={shuttle.id}>
<Table.Cell><b>{shuttle.name}</b></Table.Cell>
<Table.Cell textAlign='center'>
<ShuttleFactionView factionId={shuttle.faction} size={3} />
</Table.Cell>
<Table.Cell>
<Table striped selectable singleLine compact='very' style={{ margin: '0 auto' }}>
<Table.Body>
{shuttle.seats.map((seat, seatNum) =>
<SeatAssignmentRow key={seatNum} shuttleId={shuttle.id} seatNum={seatNum} seat={seat} />
)}
</Table.Body>
</Table>
</Table.Cell>
<Table.Cell textAlign='center'>
{shuttleScores[shuttle.id]?.chance > 0 ? <b>{Math.floor(shuttleScores[shuttle.id].chance*100)}%</b> : <></>}
</Table.Cell>
<Table.Cell textAlign='right'>
<Button compact icon='ban' content='Dismiss' onClick={() => dismissShuttle(shuttle.id)} />
</Table.Cell>
</Table.Row>
))}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan={3}>
<Button compact icon='backward' content='Change Missions' onClick={() => props.setActiveStep('missions')} />
</Table.HeaderCell>
<Table.HeaderCell colSpan={3} textAlign='right'>
{data.length > 0 && (<Button compact icon='rocket' color='green' content='Recommend Crew' onClick={() => props.recommendShuttlers()} />)}
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
{editAssignment && (<SeatAssignmentPicker />)}
</React.Fragment>
);
function dismissShuttle(shuttleId: string): void {
shuttlers.shuttles.find(shuttle => shuttle.id === shuttleId).priority = 0;
setShuttlers({...shuttlers});
}
function updateAssignment(shuttleId: string, seatNum: number, assignedCrew: any, locked: boolean): void {
// Unassign crew from previously assigned seat, if necessary
if (assignedCrew) {
const current = assigned.find(seat => seat.assignedId === assignedCrew.id);
if (current) {
current.assignedId = -1;
current.assignedSymbol = '';
current.seatScore = 0;
current.locked = false;
}
}
const seated = assigned.find(seat => seat.shuttleId === shuttleId && seat.seatNum === seatNum);
if (assignedCrew && !seated) {
assigned.push({
shuttleId,
seatNum,
ssId: assignedCrew.ssId,
assignedId: assignedCrew.id,
assignedSymbol: assignedCrew.symbol,
seatScore: assignedCrew.score,
locked
});
}
else if (assignedCrew) {
seated.assignedId = assignedCrew.id;
seated.assignedSymbol = assignedCrew.symbol;
seated.seatScore = assignedCrew.score;
seated.locked = locked;
}
else {
seated.assignedId = -1;
seated.assignedSymbol = '';
seated.seatScore = 0;
seated.locked = false;
}
setAssigned([...assigned]);
}
function toggleAssignmentLock(shuttleId: string, seatNum: number): void {
const seated = assigned.find(seat => seat.shuttleId === shuttleId && seat.seatNum === seatNum);
seated.locked = !seated.locked;
setAssigned([...assigned]);
}
function updateShuttleScores(): void {
const DIFFICULTY = 2000;
const newScores = [];
assigned.forEach(seated => {
if (!newScores[seated.shuttleId]) {
const seatCount = shuttlers.shuttles.find(shuttle => shuttle.id === seated.shuttleId).seats.length;
newScores[seated.shuttleId] = { chance: 0, scores: Array(seatCount).fill(0) };
}
newScores[seated.shuttleId].scores[seated.seatNum] = seated.seatScore;
const dAvgSkill = newScores[seated.shuttleId].scores.reduce((a, b) => (a + b), 0)/newScores[seated.shuttleId].scores.length;
const dChance = 1/(1+Math.pow(Math.E, 3.5*(0.5-dAvgSkill/DIFFICULTY)));
newScores[seated.shuttleId].chance = dAvgSkill > 0 ? dChance : 0;
});
setShuttleScores(newScores);
}
}
Example #26
Source File: UserMenu.tsx From watchparty with MIT License | 4 votes |
UserMenu = ({
user,
socket,
userToManage,
trigger,
displayName,
position,
disabled,
timestamp,
isChatMessage,
}: {
user?: firebase.User;
socket: Socket;
userToManage: string;
trigger: any;
icon?: string;
displayName?: string;
position?: any;
disabled: boolean;
timestamp?: string;
isChatMessage?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
return (
<Popup
className="userMenu"
trigger={trigger}
on="click"
open={isOpen}
onOpen={handleOpen}
onClose={handleClose}
position={position}
disabled={disabled}
>
<div className="userMenuHeader">{displayName}</div>
<div className="userMenuContent">
<Button.Group vertical labeled icon>
<Button
content="Kick"
negative
icon="ban"
onClick={async () => {
const token = await user?.getIdToken();
socket.emit('kickUser', {
userToBeKicked: userToManage,
uid: user?.uid,
token,
});
setIsOpen(false);
}}
/>
{isChatMessage && (
<Button
content="Delete Message"
icon="comment"
onClick={async () => {
const token = await user?.getIdToken();
socket.emit('CMD:deleteChatMessages', {
author: userToManage,
timestamp: timestamp,
uid: user?.uid,
token,
});
setIsOpen(false);
}}
/>
)}
<Button
content="Delete User's Messages"
icon="comments"
onClick={async () => {
const token = await user?.getIdToken();
socket.emit('CMD:deleteChatMessages', {
author: userToManage,
uid: user?.uid,
token,
});
setIsOpen(false);
}}
/>
</Button.Group>
</div>
</Popup>
);
}
Example #27
Source File: profile_charts.tsx From website with MIT License | 4 votes |
render() {
const {
data_ownership,
skill_distribution,
flat_skill_distribution,
r4_stars,
r5_stars,
radar_skill_rarity,
radar_skill_rarity_owned,
honordebt,
excludeFulfilled,
} = this.state;
let { demands } = this.state;
let totalHonorDebt = 0;
let readableHonorDebt = '';
if (honordebt) {
totalHonorDebt = honordebt.totalStars
.map((val, idx) => (val - honordebt.ownedStars[idx]) * CONFIG.CITATION_COST[idx])
.reduce((a, b) => a + b, 0);
let totalHonorDebtDays = totalHonorDebt / 2000;
let years = Math.floor(totalHonorDebtDays / 365);
let months = Math.floor((totalHonorDebtDays - years * 365) / 30);
let days = totalHonorDebtDays - years * 365 - months * 30;
readableHonorDebt = `${years} years ${months} months ${Math.floor(days)} days`;
}
let totalChronCost = 0;
let factionRec = [];
demands.forEach((entry) => {
let cost = entry.equipment.item_sources.map((its: any) => its.avg_cost).filter((cost) => !!cost);
if (cost && cost.length > 0) {
totalChronCost += Math.min(...cost) * entry.count;
} else {
let factions = entry.equipment.item_sources.filter((e) => e.type === 1);
if (factions && factions.length > 0) {
let fe = factionRec.find((e: any) => e.name === factions[0].name);
if (fe) {
fe.count += entry.count;
} else {
factionRec.push({
name: factions[0].name,
count: entry.count,
});
}
}
}
});
if (excludeFulfilled) {
demands = demands.filter((d) => d.count > d.have);
}
factionRec = factionRec.sort((a, b) => b.count - a.count).filter((e) => e.count > 0);
totalChronCost = Math.floor(totalChronCost);
return (
<ErrorBoundary>
<h3>Owned vs. Not Owned crew per rarity</h3>
<div style={{ height: '320px' }}>
<ResponsiveBar
data={data_ownership}
theme={themes.dark}
keys={['Owned', 'Not Owned', 'Not Owned - Portal']}
indexBy='rarity'
layout='horizontal'
margin={{ top: 50, right: 130, bottom: 50, left: 100 }}
padding={0.3}
axisBottom={{
legend: 'Number of crew',
legendPosition: 'middle',
legendOffset: 32,
}}
labelSkipWidth={12}
labelSkipHeight={12}
legends={[
{
dataFrom: 'keys',
anchor: 'bottom-right',
direction: 'column',
justify: false,
translateX: 120,
translateY: 0,
itemsSpacing: 2,
itemWidth: 100,
itemHeight: 20,
itemDirection: 'left-to-right',
symbolSize: 20,
effects: [
{
on: 'hover',
style: {
itemOpacity: 1,
},
},
],
},
]}
animate={false}
/>
</div>
<h3>Honor debt</h3>
{honordebt && (
<div>
<Table basic='very' striped>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Rarity</Table.HeaderCell>
<Table.HeaderCell>Required stars</Table.HeaderCell>
<Table.HeaderCell>Honor cost</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{honordebt.totalStars.map((val, idx) => (
<Table.Row key={idx}>
<Table.Cell>
<Header as='h4'>{CONFIG.RARITIES[idx + 1].name}</Header>
</Table.Cell>
<Table.Cell>
{val - honordebt.ownedStars[idx]}{' '}
<span>
<i>
({honordebt.ownedStars[idx]} / {val})
</i>
</span>
</Table.Cell>
<Table.Cell>{(val - honordebt.ownedStars[idx]) * CONFIG.CITATION_COST[idx]}</Table.Cell>
</Table.Row>
))}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell />
<Table.HeaderCell>
Owned {honordebt.ownedStars.reduce((a, b) => a + b, 0)} out of {honordebt.totalStars.reduce((a, b) => a + b, 0)}
</Table.HeaderCell>
<Table.HeaderCell>{totalHonorDebt}</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
<Message info>
<Message.Header>{readableHonorDebt}</Message.Header>
<p>That's how long will it take you to max all remaining crew in the vault at 2000 honor / day</p>
</Message>
</div>
)}
<h3>Items required to level all owned crew</h3>
<h5>Note: this may over-include already equipped items from previous level bands for certain crew</h5>
<Message info>
<Message.Header>Cost and faction recommendations</Message.Header>
<p>
Total chroniton cost to farm all these items: {totalChronCost}{' '}
<span style={{ display: 'inline-block' }}>
<img src={`${process.env.GATSBY_ASSETS_URL}atlas/energy_icon.png`} height={14} />
</span>
</p>
{honordebt && (
<p>
Total number of credits required to craft all the recipes: {honordebt.craftCost}{' '}
<span style={{ display: 'inline-block' }}>
<img src={`${process.env.GATSBY_ASSETS_URL}atlas/soft_currency_icon.png`} height={14} />
</span>
</p>
)}
</Message>
<h4>Factions with most needed non-mission items</h4>
<ul>
{factionRec.map((e) => (
<li key={e.name}>
{e.name}: {e.count} items
</li>
))}
</ul>
<div>
<Checkbox
label='Exclude already fulfilled'
onChange={() => this.setState({ excludeFulfilled: !excludeFulfilled })}
checked={this.state.excludeFulfilled}
/>
<Grid columns={3} centered padded>
{demands.map((entry, idx) => (
<Grid.Column key={idx}>
<Popup
trigger={
<Header
style={{ display: 'flex', cursor: 'zoom-in' }}
icon={
<ItemDisplay
src={`${process.env.GATSBY_ASSETS_URL}${entry.equipment.imageUrl}`}
size={48}
maxRarity={entry.equipment.rarity}
rarity={entry.equipment.rarity}
/>
}
content={entry.equipment.name}
subheader={`Need ${entry.count} ${entry.factionOnly ? ' (FACTION)' : ''} (have ${entry.have})`}
/>
}
header={CONFIG.RARITIES[entry.equipment.rarity].name + ' ' + entry.equipment.name}
content={<ItemSources item_sources={entry.equipment.item_sources} />}
on='click'
wide
/>
</Grid.Column>
))}
</Grid>
</div>
<h3>Skill coverage per rarity (yours vs. every crew in vault)</h3>
<div>
<div style={{ height: '320px', width: '50%', display: 'inline-block' }}>
<ResponsiveRadar
data={radar_skill_rarity_owned}
theme={themes.dark}
keys={['Common', 'Uncommon', 'Rare', 'Super Rare', 'Legendary']}
indexBy='name'
maxValue='auto'
margin={{ top: 70, right: 80, bottom: 40, left: 80 }}
curve='linearClosed'
borderWidth={2}
borderColor={{ from: 'color' }}
gridLevels={5}
gridShape='circular'
gridLabelOffset={36}
enableDots={true}
dotSize={10}
dotColor={{ theme: 'background' }}
dotBorderWidth={2}
dotBorderColor={{ from: 'color' }}
enableDotLabel={true}
dotLabel='value'
dotLabelYOffset={-12}
colors={{ scheme: 'nivo' }}
fillOpacity={0.25}
blendMode='multiply'
animate={true}
motionStiffness={90}
motionDamping={15}
isInteractive={true}
legends={[
{
anchor: 'top-left',
direction: 'column',
translateX: -50,
translateY: -40,
itemWidth: 80,
itemHeight: 20,
itemTextColor: '#999',
symbolSize: 12,
symbolShape: 'circle',
effects: [
{
on: 'hover',
style: {
itemTextColor: '#000',
},
},
],
},
]}
/>
</div>
<div style={{ height: '320px', width: '50%', display: 'inline-block' }}>
<ResponsiveRadar
data={radar_skill_rarity}
theme={themes.dark}
keys={['Common', 'Uncommon', 'Rare', 'Super Rare', 'Legendary']}
indexBy='name'
maxValue='auto'
margin={{ top: 70, right: 80, bottom: 40, left: 80 }}
curve='linearClosed'
borderWidth={2}
borderColor={{ from: 'color' }}
gridLevels={5}
gridShape='circular'
gridLabelOffset={36}
enableDots={true}
dotSize={10}
dotColor={{ theme: 'background' }}
dotBorderWidth={2}
dotBorderColor={{ from: 'color' }}
enableDotLabel={true}
dotLabel='value'
dotLabelYOffset={-12}
colors={{ scheme: 'nivo' }}
fillOpacity={0.25}
blendMode='multiply'
animate={true}
motionStiffness={90}
motionDamping={15}
isInteractive={true}
legends={[
{
anchor: 'top-left',
direction: 'column',
translateX: -50,
translateY: -40,
itemWidth: 80,
itemHeight: 20,
itemTextColor: '#999',
symbolSize: 12,
symbolShape: 'circle',
effects: [
{
on: 'hover',
style: {
itemTextColor: '#000',
},
},
],
},
]}
/>
</div>
</div>
<h3>Number of stars (fused rarity) for your Super Rare and Legendary crew</h3>
<div>
<div style={{ height: '320px', width: '50%', display: 'inline-block' }}>
<ResponsivePie
data={r4_stars}
theme={themes.dark}
margin={{ top: 40, right: 80, bottom: 80, left: 80 }}
innerRadius={0.2}
padAngle={2}
cornerRadius={2}
borderWidth={1}
animate={false}
slicesLabelsTextColor='#333333'
legends={[
{
anchor: 'bottom',
direction: 'row',
translateY: 56,
itemWidth: 100,
itemHeight: 18,
itemTextColor: '#999',
symbolSize: 18,
symbolShape: 'circle',
effects: [
{
on: 'hover',
style: {
itemTextColor: '#000',
},
},
],
},
]}
/>
</div>
<div style={{ height: '320px', width: '50%', display: 'inline-block' }}>
<ResponsivePie
data={r5_stars}
theme={themes.dark}
margin={{ top: 40, right: 80, bottom: 80, left: 80 }}
innerRadius={0.2}
padAngle={2}
cornerRadius={2}
borderWidth={1}
animate={false}
slicesLabelsTextColor='#333333'
legends={[
{
anchor: 'bottom',
direction: 'row',
translateY: 56,
itemWidth: 100,
itemHeight: 18,
itemTextColor: '#999',
symbolSize: 18,
symbolShape: 'circle',
effects: [
{
on: 'hover',
style: {
itemTextColor: '#000',
},
},
],
},
]}
/>
</div>
</div>
<h3>Skill distribution for owned crew (number of characters per skill combos Primary > Secondary)</h3>
<Checkbox label='Include tertiary skill' onChange={() => this._onIncludeTertiary()} checked={this.state.includeTertiary} />
<div>
<div style={{ height: '420px', width: '50%', display: 'inline-block' }}>
<ResponsiveSunburst
data={skill_distribution}
theme={themes.dark}
margin={{ top: 40, right: 20, bottom: 20, left: 20 }}
identity='name'
value='loc'
cornerRadius={2}
borderWidth={1}
borderColor='white'
colors={{ scheme: 'nivo' }}
childColor={{ from: 'color' }}
animate={true}
motionStiffness={90}
motionDamping={15}
isInteractive={true}
/>
</div>
<div style={{ height: '420px', width: '50%', display: 'inline-block' }}>
<ResponsiveBar
data={flat_skill_distribution}
keys={['Count']}
theme={themes.dark}
indexBy='name'
layout='horizontal'
margin={{ top: 50, right: 130, bottom: 50, left: 100 }}
padding={0.3}
axisBottom={{
legend: 'Number of crew',
legendPosition: 'middle',
legendOffset: 32,
}}
labelSkipWidth={12}
labelSkipHeight={12}
legends={[
{
dataFrom: 'keys',
anchor: 'bottom-right',
direction: 'column',
justify: false,
translateX: 120,
translateY: 0,
itemsSpacing: 2,
itemWidth: 100,
itemHeight: 20,
itemDirection: 'left-to-right',
symbolSize: 20,
effects: [
{
on: 'hover',
style: {
itemOpacity: 1,
},
},
],
},
]}
animate={false}
/>
</div>
</div>
</ErrorBoundary>
);
}
Example #28
Source File: SubtitleModal.tsx From watchparty with MIT License | 4 votes |
render() {
const { closeModal } = this.props;
return (
<Modal open={true} onClose={closeModal as any}>
<Modal.Header>
Subtitles are {Boolean(this.props.currentSubtitle) ? 'on' : 'off'}
<Button
style={{ float: 'right' }}
color="red"
title="Remove Subtitles"
disabled={
!Boolean(this.props.currentSubtitle) || !this.props.haveLock()
}
icon
onClick={() => {
this.props.socket.emit('CMD:subtitle', null);
}}
>
<Icon name="trash" />
</Button>
</Modal.Header>
<Modal.Content image>
<Modal.Description>
{process.env.NODE_ENV === 'development' && (
<div style={{ maxWidth: '600px' }}>
{this.props.currentSubtitle}
</div>
)}
<Grid columns={2}>
<Grid.Column>
<Popup
content="Upload a .srt subtitle file for this video"
trigger={
<Button
color="violet"
icon
labelPosition="left"
fluid
onClick={() => this.uploadSubtitle()}
disabled={!this.props.haveLock()}
>
<Icon name="closed captioning" />
Upload Subtitles
</Button>
}
/>
</Grid.Column>
{this.props.src.startsWith('http') && (
<Grid.Column>
<div style={{ display: 'flex' }}>
<Button
loading={this.state.loading}
color="green"
disabled={!this.props.haveLock()}
icon
labelPosition="left"
fluid
onClick={async () => {
this.setState({ loading: true });
const resp = await window.fetch(
serverPath + '/searchSubtitles?url=' + this.props.src
);
const json = await resp.json();
this.setState({ searchResults: json });
this.setState({ loading: false });
}}
>
<Icon name="search" />
Search OpenSubtitles
</Button>
{this.props.beta && (
<Button
loading={this.state.loading}
color="green"
disabled={!this.props.haveLock()}
icon
labelPosition="left"
fluid
onClick={async () => {
this.setState({ loading: true });
const resp = await window.fetch(
serverPath +
'/searchSubtitles?title=' +
this.props.getMediaDisplayName(this.props.src)
);
const json = await resp.json();
this.setState({ searchResults: json });
this.setState({ loading: false });
}}
>
<Icon name="search" />
Search by Title
</Button>
)}
</div>
</Grid.Column>
)}
</Grid>
<div>
{this.state.searchResults.map((result: any) => (
<div>
<Radio
label={result.SubFileName}
name="radioGroup"
value={result.SubDownloadLink}
checked={this.props.currentSubtitle?.includes(
result.SubDownloadLink
)}
onChange={(e, { value }) => {
this.props.socket.emit(
'CMD:subtitle',
serverPath + '/downloadSubtitles?url=' + value
);
}}
/>
</div>
))}
</div>
{/* TODO add a spinner */}
</Modal.Description>
</Modal.Content>
</Modal>
);
}
Example #29
Source File: SettingsTab.tsx From watchparty with MIT License | 4 votes |
SettingsTab = ({
hide,
user,
roomLock,
setRoomLock,
socket,
isSubscriber,
owner,
vanity,
setVanity,
roomLink,
password,
setPassword,
isChatDisabled,
setIsChatDisabled,
clearChat,
roomTitle,
roomDescription,
roomTitleColor,
mediaPath,
setMediaPath,
}: SettingsTabProps) => {
const [updateTS, setUpdateTS] = useState(0);
const [permModalOpen, setPermModalOpen] = useState(false);
const [validVanity, setValidVanity] = useState(true);
const [validVanityLoading, setValidVanityLoading] = useState(false);
const [adminSettingsChanged, setAdminSettingsChanged] = useState(false);
const [roomTitleInput, setRoomTitleInput] = useState<string | undefined>(undefined);
const [roomDescriptionInput, setRoomDescriptionInput] = useState<
string | undefined
>(undefined);
const [roomTitleColorInput, setRoomTitleColorInput] = useState<
string | undefined
>('');
const setRoomState = useCallback(
async (data: any) => {
const token = await user?.getIdToken();
socket.emit('CMD:setRoomState', {
uid: user?.uid,
token,
...data,
});
},
[socket, user]
);
const setRoomOwner = useCallback(
async (data: any) => {
const token = await user?.getIdToken();
socket.emit('CMD:setRoomOwner', {
uid: user?.uid,
token,
...data,
});
},
[socket, user]
);
const checkValidVanity = useCallback(
async (input: string) => {
if (!input) {
setValidVanity(true);
return;
}
setValidVanity(false);
setValidVanityLoading(true);
const response = await axios.get(serverPath + '/resolveRoom/' + input);
const data = response.data;
setValidVanityLoading(false);
if (
data &&
data.vanity &&
data.vanity !== roomLink.split('/').slice(-1)[0]
) {
// Already exists and doesn't match current room
setValidVanity(false);
} else {
setValidVanity(true);
}
},
[setValidVanity, roomLink]
);
const disableLocking =
!Boolean(user) || Boolean(roomLock && roomLock !== user?.uid);
const disableOwning = !Boolean(user) || Boolean(owner && owner !== user?.uid);
return (
<div
style={{
display: hide ? 'none' : 'block',
color: 'white',
overflow: 'scroll',
padding: '8px',
}}
>
{permModalOpen && (
<PermanentRoomModal
closeModal={() => setPermModalOpen(false)}
></PermanentRoomModal>
)}
<div className="sectionHeader">Room Settings</div>
{!user && (
<Message color="yellow" size="tiny">
You need to be signed in to change these settings.
</Message>
)}
<SettingRow
icon={roomLock ? 'lock' : 'lock open'}
name={`Lock Room`}
description="Only the person who locked the room can control the video."
checked={Boolean(roomLock)}
disabled={disableLocking && disableOwning}
onChange={(_e, data) => setRoomLock(data.checked)}
/>
{
<SettingRow
icon={'clock'}
name={`Make Room Permanent`}
description={
'Prevent this room from expiring. This also unlocks additional room features.'
}
helpIcon={
<Icon
name="help circle"
onClick={() => setPermModalOpen(true)}
style={{ cursor: 'pointer' }}
></Icon>
}
checked={Boolean(owner)}
disabled={disableOwning}
onChange={(_e, data) => setRoomOwner({ undo: !data.checked })}
/>
}
{owner && owner === user?.uid && (
<div className="sectionHeader">Admin Settings</div>
)}
{owner && owner === user?.uid && (
<SettingRow
icon={'key'}
name={`Set Room Password`}
description="Users must know this password in order to join the room."
content={
<Input
value={password}
size="mini"
onChange={(e) => {
setAdminSettingsChanged(true);
setPassword(e.target.value);
}}
fluid
/>
}
disabled={false}
/>
)}
{owner && owner === user?.uid && (
<SettingRow
icon={'folder'}
name={`Set Room Media Source`}
description="Set a media source URL with files to replace the default examples. Supports S3 buckets and nginx file servers."
content={
<Input
value={mediaPath}
size="mini"
onChange={(e) => {
setAdminSettingsChanged(true);
setMediaPath(e.target.value);
}}
fluid
/>
}
disabled={false}
/>
)}
{owner && owner === user?.uid && (
<SettingRow
icon={'i cursor'}
name={`Disable Chat`}
description="Prevent users from sending messages in chat."
checked={Boolean(isChatDisabled)}
disabled={false}
onChange={(_e, data) => {
setAdminSettingsChanged(true);
setIsChatDisabled(Boolean(data.checked));
}}
/>
)}
{owner && owner === user?.uid && (
<SettingRow
icon={'i delete'}
name={`Clear Chat`}
description="Delete all existing chat messages"
disabled={false}
content={
<Button
color="red"
icon
labelPosition="left"
onClick={() => clearChat()}
>
<Icon name="delete" />
Delete Messages
</Button>
}
/>
)}
{owner && owner === user?.uid && (
<SettingRow
icon={'linkify'}
name={`Set Custom Room URL`}
description="Set a custom URL for this room. Inappropriate names may be revoked."
checked={Boolean(roomLock)}
disabled={!isSubscriber}
subOnly={true}
content={
<React.Fragment>
<Input
value={vanity}
disabled={!isSubscriber}
onChange={(e) => {
setAdminSettingsChanged(true);
checkValidVanity(e.target.value);
setVanity(e.target.value);
}}
label={<Label>{`${window.location.origin}/r/`}</Label>}
loading={validVanityLoading}
fluid
size="mini"
icon
action={
validVanity ? (
<Icon name="checkmark" color="green" />
) : (
<Icon name="close" color="red" />
)
}
></Input>
</React.Fragment>
}
/>
)}
{owner && owner === user?.uid && (
<SettingRow
icon={'pencil'}
name={`Set Room Title, Description & Color`}
description="Set the room title, description and title color to be displayed in the top bar."
disabled={!isSubscriber}
subOnly={true}
content={
<React.Fragment>
<div style={{ display: 'flex', marginBottom: 2 }}>
<Input
style={{ marginRight: 3, flexGrow: 1 }}
value={roomTitleInput ?? roomTitle}
disabled={!isSubscriber}
maxLength={roomTitleMaxCharLength}
onChange={(e) => {
setAdminSettingsChanged(true);
setRoomTitleInput(e.target.value);
}}
placeholder={`Title (max. ${roomTitleMaxCharLength} characters)`}
fluid
size="mini"
icon
></Input>
<Popup
content={
<React.Fragment>
<h5>Edit Title Color</h5>
<HexColorPicker
color={
roomTitleColorInput ||
roomTitleColor ||
defaultRoomTitleColor
}
onChange={(e) => {
setAdminSettingsChanged(true);
setRoomTitleColorInput(e);
}}
/>
<div
style={{
marginTop: 8,
paddingLeft: 4,
borderLeft: `24px solid ${roomTitleColorInput}`,
}}
>
{roomTitleColorInput?.toUpperCase()}
</div>
</React.Fragment>
}
on="click"
trigger={
<Button
icon
color="teal"
size="tiny"
style={{ margin: 0 }}
disabled={!isSubscriber}
>
<Icon name="paint brush" />
</Button>
}
/>
</div>
<Input
style={{ marginBottom: 2 }}
value={roomDescriptionInput ?? roomDescription}
disabled={!isSubscriber}
maxLength={roomDescriptionMaxCharLength}
onChange={(e) => {
setAdminSettingsChanged(true);
setRoomDescriptionInput(e.target.value);
}}
placeholder={`Description (max. ${roomDescriptionMaxCharLength} characters)`}
fluid
size="mini"
icon
></Input>
</React.Fragment>
}
/>
)}
<div
style={{
borderTop: '3px dashed white',
marginTop: 10,
marginBottom: 10,
}}
/>
{owner && owner === user?.uid && (
<Button
primary
disabled={!validVanity || !adminSettingsChanged}
labelPosition="left"
icon
fluid
onClick={() => {
setRoomState({
vanity: vanity,
password: password,
isChatDisabled: isChatDisabled,
roomTitle: roomTitleInput ?? roomTitle,
roomDescription: roomDescriptionInput ?? roomDescription,
roomTitleColor:
roomTitleColorInput || roomTitleColor || defaultRoomTitleColor,
mediaPath: mediaPath,
});
setAdminSettingsChanged(false);
}}
>
<Icon name="save" />
Save Admin Settings
</Button>
)}
<div className="sectionHeader">Local Settings</div>
<SettingRow
updateTS={updateTS}
icon="bell"
name="Disable chat notification sound"
description="Don't play a sound when a chat message is sent while you're on another tab"
checked={Boolean(getCurrentSettings().disableChatSound)}
disabled={false}
onChange={(_e, data) => {
updateSettings(
JSON.stringify({
...getCurrentSettings(),
disableChatSound: data.checked,
})
);
setUpdateTS(Number(new Date()));
}}
/>
</div>
);
}