2020-08-13 13:45:18 +03:00
import React from 'react' ;
2021-07-21 16:50:26 +03:00
import { Transition } from '@headlessui/react'
2020-08-13 13:45:18 +03:00
export default class SiteSwitcher extends React . Component {
constructor ( ) {
super ( )
2021-12-06 16:20:26 +03:00
this . handleClick = this . handleClick . bind ( this ) ;
2021-02-23 16:49:56 +03:00
this . handleKeydown = this . handleKeydown . bind ( this ) ;
this . populateSites = this . populateSites . bind ( this ) ;
2021-12-06 16:20:26 +03:00
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 ( ) {
2021-02-23 16:49:56 +03:00
this . populateSites ( ) ;
2021-12-06 16:20:26 +03:00
this . siteSwitcherButton . current . addEventListener ( "click" , this . toggle ) ;
2021-02-23 16:49:56 +03:00
document . addEventListener ( "keydown" , this . handleKeydown ) ;
2021-12-06 16:20:26 +03:00
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 ( ) {
2021-12-06 16:20:26 +03:00
this . siteSwitcherButton . current . removeEventListener ( "click" , this . toggle ) ;
2021-02-23 16:49:56 +03:00
document . removeEventListener ( "keydown" , this . handleKeydown ) ;
2021-12-06 16:20:26 +03:00
document . removeEventListener ( 'click' , this . handleClick , false ) ;
2020-08-13 13:45:18 +03:00
}
2021-02-23 16:49:56 +03:00
populateSites ( ) {
2021-04-07 11:06:33 +03:00
if ( ! this . props . loggedIn ) return ;
2021-02-23 16:49:56 +03:00
fetch ( '/api/sites' )
. then ( response => {
if ( ! response . ok ) { throw response }
return response . json ( )
} )
2022-04-18 12:32:01 +03:00
. then ( ( sites ) => this . setState (
{
loading : false ,
sites : sites . data . map ( s => s . domain )
} ) )
2021-02-23 16:49:56 +03:00
. catch ( ( e ) => this . setState ( { loading : false , error : e } ) )
}
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.
2020-08-13 13:45:18 +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.
2020-08-13 13:45:18 +03:00
if ( ! this . state . open ) return ;
2021-12-06 16:20:26 +03:00
// In any other case, close it.
this . setState ( { open : false } ) ;
2020-08-13 13:45:18 +03:00
}
2021-02-23 16:49:56 +03:00
handleKeydown ( e ) {
2021-12-22 10:54:09 +03:00
if ( ! this . props . loggedIn ) return ;
2022-04-18 12:32:01 +03:00
2021-10-11 15:48:19 +03:00
const { site } = this . props ;
2021-02-23 16:49:56 +03:00
const { sites } = this . state ;
2021-04-01 10:12:01 +03:00
if ( e . target . tagName === 'INPUT' ) return true ;
2021-04-01 10:25:24 +03:00
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 )
2021-03-04 10:50:15 +03:00
if ( 1 <= siteNum && siteNum <= 9 && siteNum <= sites . length && sites [ siteNum - 1 ] !== site . domain ) {
2021-02-23 16:49:56 +03:00
window . location = ` / ${ encodeURIComponent ( sites [ siteNum - 1 ] ) } `
}
}
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 .
* /
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
2020-08-13 13:45:18 +03:00
if ( ! this . props . loggedIn ) return ;
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 ) {
2021-02-23 16:49:56 +03:00
this . populateSites ( ) ;
2020-08-13 13:45:18 +03:00
}
}
2021-02-23 16:49:56 +03:00
renderSiteLink ( domain , index ) {
2020-12-29 12:00:41 +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 (
2021-02-23 16:49:56 +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 } ` } >
< span >
2021-11-26 12:59:04 +03:00
< img src = { ` /favicon/sources/ ${ encodeURIComponent ( domain ) } ` } className = "inline w-4 mr-2 align-middle" / >
2021-06-16 15:00:07 +03:00
< 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 >
2021-12-22 10:54:09 +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-02-23 22:48:33 +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" >
< 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" >
< svg 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" fill = "currentColor" viewBox = "0 0 20 20" xmlns = "http://www.w3.org/2000/svg" > < path fillRule = "evenodd" d = "M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule = "evenodd" > < / p a t h > < / s v g >
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 ) {
return < div className = "px-4 py-6" > < div className = "loading sm mx-auto" > < div > < / d i v > < / d i v > < / d i v >
} else if ( this . state . error ) {
2020-12-16 12:57:28 +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" >
{ [ this . props . site . domain ] . map ( this . renderSiteLink . bind ( this ) ) }
< / 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 >
2021-06-16 15:00:07 +03:00
{ this . renderSettingsLink ( ) }
2020-08-13 13:45:18 +03:00
< div className = "py-1" >
{ this . state . sites . map ( this . renderSiteLink . bind ( this ) ) }
< / 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-04-18 12:32:01 +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" >
View all
< / a >
2020-08-13 13:45:18 +03:00
< / R e a c t . F r a g m e n t >
)
}
}
renderArrow ( ) {
if ( this . props . loggedIn ) {
return (
2021-07-21 16:50:26 +03:00
< svg className = "-mr-1 ml-1 md:ml-2 h-5 w-5" viewBox = "0 0 20 20" fill = "currentColor" >
2020-08-13 13:45:18 +03:00
< path fillRule = "evenodd" d = "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule = "evenodd" / >
< / s v g >
)
}
}
render ( ) {
2020-12-16 12:57:28 +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" >
2021-12-06 16:20:26 +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 } ` } >
2020-08-13 13:45:18 +03:00
2021-07-21 16:50:26 +03:00
< img src = { ` https://icons.duckduckgo.com/ip3/ ${ this . props . site . domain } .ico ` } onError = { ( e ) => { e . target . onerror = null ; e . target . src = "https://icons.duckduckgo.com/ip3/placeholder.ico" } } referrerPolicy = "no-referrer" className = "inline w-4 mr-1 md:mr-2 align-middle" / >
2021-08-23 12:39:43 +03:00
< span className = "hidden sm:inline-block" > { this . props . site . domain } < / s p a n >
2020-08-13 13:45:18 +03:00
{ this . renderArrow ( ) }
< / 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"
>
< div className = "origin-top-left absolute left-0 mt-2 w-64 rounded-md shadow-lg" ref = { node => this . dropDownNode = node } >
2020-12-16 12:57:28 +03:00
< div className = "rounded-md bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5" >
2020-08-13 13:45:18 +03:00
{ this . renderDropdown ( ) }
< / d i v >
< / d i v >
< / T r a n s i t i o n >
< / d i v >
)
}
}