2022-10-04 13:20:51 +03:00
/ * *
* @ prettier
* /
import React from 'react'
2021-07-21 16:50:26 +03:00
import { Transition } from '@headlessui/react'
2022-10-04 13:20:51 +03:00
import { Cog8ToothIcon , ChevronDownIcon } from '@heroicons/react/20/solid'
function Favicon ( { domain , className } ) {
return (
< img
src = { ` /favicon/sources/ ${ encodeURIComponent ( domain ) } ` }
onError = { ( e ) => {
e . target . onerror = null
e . target . src = '/favicon/sources/placeholder'
} }
referrerPolicy = "no-referrer"
className = { className }
/ >
)
}
2020-08-13 13:45:18 +03:00
export default class SiteSwitcher extends React . Component {
constructor ( ) {
super ( )
2022-10-04 13:20:51 +03:00
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 ( )
2020-08-13 13:45:18 +03:00
this . state = {
open : false ,
sites : null ,
error : null ,
loading : true
}
}
componentDidMount ( ) {
2022-10-04 13:20:51 +03:00
this . populateSites ( )
this . siteSwitcherButton . current . addEventListener ( 'click' , this . toggle )
document . addEventListener ( 'keydown' , this . handleKeydown )
document . addEventListener ( 'click' , this . handleClick , false )
2020-08-13 13:45:18 +03:00
}
2022-04-18 12:32:01 +03:00
2020-08-13 13:45:18 +03:00
componentWillUnmount ( ) {
2022-10-04 13:20:51 +03:00
this . siteSwitcherButton . current . removeEventListener ( 'click' , this . toggle )
document . removeEventListener ( 'keydown' , this . handleKeydown )
document . removeEventListener ( 'click' , this . handleClick , false )
2020-08-13 13:45:18 +03:00
}
2021-02-23 16:49:56 +03:00
populateSites ( ) {
2022-10-04 13:20:51 +03:00
if ( ! this . props . loggedIn ) return
2021-04-07 11:06:33 +03:00
2021-02-23 16:49:56 +03:00
fetch ( '/api/sites' )
2022-10-04 13:20:51 +03:00
. then ( ( response ) => {
if ( ! response . ok ) {
throw response
}
2021-02-23 16:49:56 +03:00
return response . json ( )
} )
2022-10-04 13:20:51 +03:00
. then ( ( sites ) =>
this . setState ( {
2022-04-18 12:32:01 +03:00
loading : false ,
2022-10-04 13:20:51 +03:00
sites : sites . data . map ( ( s ) => s . domain )
} )
)
. catch ( ( e ) => this . setState ( { loading : false , error : e } ) )
2021-02-23 16:49:56 +03:00
}
2020-08-13 13:45:18 +03:00
handleClick ( e ) {
2021-12-06 16:20:26 +03:00
// If this is an interaction with the dropdown menu itself, do nothing.
2022-10-04 13:20:51 +03:00
if ( this . dropDownNode && this . dropDownNode . contains ( e . target ) ) return
2021-12-06 16:20:26 +03:00
// If the dropdown is not open, do nothing.
2022-10-04 13:20:51 +03:00
if ( ! this . state . open ) return
2020-08-13 13:45:18 +03:00
2021-12-06 16:20:26 +03:00
// In any other case, close it.
2022-10-04 13:20:51 +03:00
this . setState ( { open : false } )
2020-08-13 13:45:18 +03:00
}
2021-02-23 16:49:56 +03:00
handleKeydown ( e ) {
2022-10-04 13:20:51 +03:00
if ( ! this . props . loggedIn ) return
2022-04-18 12:32:01 +03:00
2022-10-04 13:20:51 +03:00
const { site } = this . props
const { sites } = this . state
2021-02-23 16:49:56 +03:00
2022-10-04 13:20:51 +03:00
if ( e . target . tagName === 'INPUT' ) return true
if (
e . ctrlKey ||
e . metaKey ||
e . altKey ||
e . isComposing ||
e . keyCode === 229 ||
! sites
)
return
2021-02-23 16:49:56 +03:00
const siteNum = parseInt ( e . key )
2022-10-04 13:20:51 +03:00
if (
1 <= siteNum &&
siteNum <= 9 &&
siteNum <= sites . length &&
sites [ siteNum - 1 ] !== site . domain
) {
window . location = ` / ${ encodeURIComponent ( sites [ siteNum - 1 ] ) } `
2021-02-23 16:49:56 +03:00
}
}
2021-12-06 16:20:26 +03:00
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 .
2022-04-18 12:32:01 +03:00
*
2021-12-06 16:20:26 +03:00
* 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 .
* /
2022-10-04 13:20:51 +03:00
e . stopPropagation ( )
e . preventDefault ( )
if ( ! this . props . loggedIn ) return
2020-08-13 13:45:18 +03:00
2021-12-06 16:20:26 +03:00
this . setState ( ( prevState ) => ( {
open : ! prevState . open
} ) )
2020-08-13 13:45:18 +03:00
2021-12-22 10:54:09 +03:00
if ( this . props . loggedIn && ! this . state . sites ) {
2022-10-04 13:20:51 +03:00
this . populateSites ( )
2020-08-13 13:45:18 +03:00
}
}
2021-02-23 16:49:56 +03:00
renderSiteLink ( domain , index ) {
2022-10-04 13:20:51 +03:00
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'
2021-12-22 10:54:09 +03:00
const showHotkey = ! this . props . loggedIn
2020-08-13 13:45:18 +03:00
return (
2022-10-04 13:20:51 +03:00
< a
href = {
domain === this . props . site . domain
? null
: ` / ${ encodeURIComponent ( domain ) } `
}
key = { domain }
className = { ` flex items-center justify-between truncate px-4 py-2 md:text-sm leading-5 text-gray-700 dark:text-gray-300 ${ extraClass } ` }
>
2021-02-23 16:49:56 +03:00
< span >
2022-10-04 13:20:51 +03:00
< Favicon
domain = { domain }
className = "inline w-4 mr-2 align-middle"
> < / F a v i c o n >
< span className = "truncate inline-block align-middle max-w-3xs pr-2" >
{ domain }
< / s p a n >
2021-02-23 16:49:56 +03:00
< / s p a n >
2022-10-04 13:20:51 +03:00
{ showHotkey ? index < 9 && < span > { index + 1 } < / s p a n > : n u l l }
2020-08-13 13:45:18 +03:00
< / a >
)
}
2021-06-16 15:00:07 +03:00
renderSettingsLink ( ) {
2022-10-04 13:20:51 +03:00
if (
[ 'owner' , 'admin' , 'super_admin' ] . includes ( this . props . currentUserRole )
) {
2021-06-16 15:00:07 +03:00
return (
< React . Fragment >
< div className = "py-1" >
2022-10-04 13:20:51 +03:00
< a
href = { ` / ${ encodeURIComponent ( this . props . site . domain ) } /settings ` }
className = "group flex items-center px-4 py-2 md:text-sm leading-5 text-gray-700 dark:text-gray-300 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"
role = "menuitem"
>
< Cog8ToothIcon className = "mr-2 h-4 w-4 text-gray-500 dark:text-gray-200 group-hover:text-gray-600 dark:group-hover:text-gray-400 group-focus:text-gray-500 dark:group-focus:text-gray-200" / >
2021-06-16 15:00:07 +03:00
Site settings
< / a >
< / d i v >
< div className = "border-t border-gray-200 dark:border-gray-500" > < / d i v >
< / R e a c t . F r a g m e n t >
)
}
}
2021-12-22 10:54:09 +03:00
/ * *
* 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 .
* /
2020-08-13 13:45:18 +03:00
renderDropdown ( ) {
if ( this . state . loading ) {
2022-10-04 13:20:51 +03:00
return (
< div className = "px-4 py-6" >
< div className = "loading sm mx-auto" >
< div > < / d i v >
< / d i v >
< / d i v >
)
2020-08-13 13:45:18 +03:00
} else if ( this . state . error ) {
2022-10-04 13:20:51 +03:00
return (
< div className = "mx-auto px-4 py-6 dark:text-gray-100" >
Something went wrong , try again
< / d i v >
)
2021-12-22 10:54:09 +03:00
} else if ( ! this . props . loggedIn ) {
return (
< React . Fragment >
< div className = "py-1" >
2022-10-04 13:20:51 +03:00
{ [ this . props . site . domain ] . map ( this . renderSiteLink . bind ( this ) ) }
2021-12-22 10:54:09 +03:00
< / d i v >
< / R e a c t . F r a g m e n t >
)
2020-08-13 13:45:18 +03:00
} else {
return (
< React . Fragment >
2022-10-04 13:20:51 +03:00
{ this . renderSettingsLink ( ) }
2020-08-13 13:45:18 +03:00
< div className = "py-1" >
2022-10-04 13:20:51 +03:00
{ this . state . sites . map ( this . renderSiteLink . bind ( this ) ) }
2020-08-13 13:45:18 +03:00
< / d i v >
2021-01-20 16:54:08 +03:00
< div className = "border-t border-gray-200 dark:border-gray-500" > < / d i v >
2022-10-04 13:20:51 +03:00
< a
href = "/sites"
className = "flex px-4 py-2 md:text-sm leading-5 text-gray-700 dark:text-gray-300 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"
role = "menuitem"
>
2022-04-18 12:32:01 +03:00
View all
< / a >
2020-08-13 13:45:18 +03:00
< / R e a c t . F r a g m e n t >
)
}
}
render ( ) {
2022-10-04 13:20:51 +03:00
const hoverClass = this . props . loggedIn
? 'hover:text-gray-500 dark:hover:text-gray-200 focus:border-blue-300 focus:ring '
: 'cursor-default'
2020-08-13 13:45:18 +03:00
return (
2021-09-21 12:08:00 +03:00
< div className = "relative inline-block text-left mr-2 sm:mr-4" >
2022-10-04 13:20:51 +03:00
< button
ref = { this . siteSwitcherButton }
className = { ` inline-flex items-center md:text-lg w-full rounded-md py-2 leading-5 font-bold text-gray-700 dark:text-gray-300 focus:outline-none transition ease-in-out duration-150 ${ hoverClass } ` }
>
< Favicon
domain = { this . props . site . domain }
className = "w-4 mr-1 md:mr-2 align-middle w-4 mr-2 align-middle"
> < / F a v i c o n >
< span className = "hidden sm:inline-block" >
{ this . props . site . domain }
< / s p a n >
{ this . props . loggedIn && (
< ChevronDownIcon className = "-mr-1 ml-1 md:ml-2 h-5 w-5" / >
) }
2020-08-13 13:45:18 +03:00
< / b u t t o n >
< Transition
show = { this . state . open }
enter = "transition ease-out duration-100 transform"
enterFrom = "opacity-0 scale-95"
enterTo = "opacity-100 scale-100"
leave = "transition ease-in duration-75 transform"
leaveFrom = "opacity-100 scale-100"
leaveTo = "opacity-0 scale-95"
>
2022-10-04 13:20:51 +03:00
< div
className = "origin-top-left absolute left-0 mt-2 w-64 rounded-md shadow-lg"
ref = { ( node ) => ( this . dropDownNode = node ) }
>
< div className = "rounded-md bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5" >
{ this . renderDropdown ( ) }
< / d i v >
2020-08-13 13:45:18 +03:00
< / d i v >
< / T r a n s i t i o n >
< / d i v >
)
}
}