/** * @prettier */ import React from 'react' import { Transition } from '@headlessui/react' import { Cog8ToothIcon, ChevronDownIcon } from '@heroicons/react/20/solid' function Favicon({ domain, className }) { return ( { e.target.onerror = null e.target.src = '/favicon/sources/placeholder' }} referrerPolicy="no-referrer" className={className} /> ) } export default class SiteSwitcher extends React.Component { constructor() { super() this.handleClick = this.handleClick.bind(this) this.handleKeydown = this.handleKeydown.bind(this) this.populateSites = this.populateSites.bind(this) this.toggle = this.toggle.bind(this) this.siteSwitcherButton = React.createRef() this.state = { open: false, sites: null, error: null, loading: true } } componentDidMount() { this.populateSites() this.siteSwitcherButton.current.addEventListener('click', this.toggle) document.addEventListener('keydown', this.handleKeydown) document.addEventListener('click', this.handleClick, false) } componentWillUnmount() { this.siteSwitcherButton.current.removeEventListener('click', this.toggle) document.removeEventListener('keydown', this.handleKeydown) document.removeEventListener('click', this.handleClick, false) } populateSites() { if (!this.props.loggedIn) return fetch('/api/sites') .then((response) => { if (!response.ok) { throw response } return response.json() }) .then((sites) => this.setState({ loading: false, sites: sites.data.map((s) => s.domain) }) ) .catch((e) => this.setState({ loading: false, error: e })) } handleClick(e) { // If this is an interaction with the dropdown menu itself, do nothing. if (this.dropDownNode && this.dropDownNode.contains(e.target)) return // If the dropdown is not open, do nothing. if (!this.state.open) return // In any other case, close it. this.setState({ open: false }) } handleKeydown(e) { if (!this.props.loggedIn) return const { site } = this.props const { sites } = this.state if (e.target.tagName === 'INPUT') return true if ( e.ctrlKey || e.metaKey || e.altKey || e.isComposing || e.keyCode === 229 || !sites ) return const siteNum = parseInt(e.key) if ( 1 <= siteNum && siteNum <= 9 && siteNum <= sites.length && sites[siteNum - 1] !== site.domain ) { window.location = `/${encodeURIComponent(sites[siteNum - 1])}` } } toggle(e) { /** * React doesn't seem to prioritise its own events when events are bubbling, and is unable to stop its events from propagating to the document's (root) event listeners which are attached on the DOM. * * A simple trick is to hook up our own click event listener via a ref node, which allows React to manage events in this situation better between the two. */ e.stopPropagation() e.preventDefault() if (!this.props.loggedIn) return this.setState((prevState) => ({ open: !prevState.open })) if (this.props.loggedIn && !this.state.sites) { this.populateSites() } } renderSiteLink(domain, index) { const extraClass = domain === this.props.site.domain ? 'font-medium text-gray-900 dark:text-gray-100 cursor-default font-bold' : 'hover:bg-gray-100 dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-900 dark:focus:text-gray-100' const showHotkey = !this.props.loggedIn return ( {domain} {showHotkey ? index < 9 && {index + 1} : null} ) } renderSettingsLink() { if ( ['owner', 'admin', 'super_admin'].includes(this.props.currentUserRole) ) { return (
Site settings
) } } /** * Render a dropdown regardless of whether the user is logged in or not. In case they are not logged in (such as in an embed), the dropdown merely contains the current domain name. */ renderDropdown() { if (this.state.loading) { return (
) } else if (this.state.error) { return (
Something went wrong, try again
) } else if (!this.props.loggedIn) { return (
{[this.props.site.domain].map(this.renderSiteLink.bind(this))}
) } else { return ( {this.renderSettingsLink()}
{this.state.sites.map(this.renderSiteLink.bind(this))}
View all
) } } render() { const hoverClass = this.props.loggedIn ? 'hover:text-gray-500 dark:hover:text-gray-200 focus:border-blue-300 focus:ring ' : 'cursor-default' return (
(this.dropDownNode = node)} >
{this.renderDropdown()}
) } }