feat: synchronized platform tabs (#165)
* feat: synchronized platform tabs * chore: rename enums to pascal case * chore: fixed typo
This commit is contained in:
+11
-8
@@ -9,6 +9,7 @@ import { SnackbarProvider } from 'notistack'
|
|||||||
import BaseRouter from './routes/routes'
|
import BaseRouter from './routes/routes'
|
||||||
import { lightTheme, darkTheme } from './theme'
|
import { lightTheme, darkTheme } from './theme'
|
||||||
import { Provider as StampsProvider } from './providers/Stamps'
|
import { Provider as StampsProvider } from './providers/Stamps'
|
||||||
|
import { Provider as PlatformProvider } from './providers/Platform'
|
||||||
|
|
||||||
const App = (): ReactElement => {
|
const App = (): ReactElement => {
|
||||||
const [themeMode, toggleThemeMode] = useState('light')
|
const [themeMode, toggleThemeMode] = useState('light')
|
||||||
@@ -36,14 +37,16 @@ const App = (): ReactElement => {
|
|||||||
<div className="App">
|
<div className="App">
|
||||||
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
|
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
|
||||||
<StampsProvider>
|
<StampsProvider>
|
||||||
<SnackbarProvider>
|
<PlatformProvider>
|
||||||
<>
|
<SnackbarProvider>
|
||||||
<CssBaseline />
|
<>
|
||||||
<Router>
|
<CssBaseline />
|
||||||
<BaseRouter />
|
<Router>
|
||||||
</Router>
|
<BaseRouter />
|
||||||
</>
|
</Router>
|
||||||
</SnackbarProvider>
|
</>
|
||||||
|
</SnackbarProvider>
|
||||||
|
</PlatformProvider>
|
||||||
</StampsProvider>
|
</StampsProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import React, { ReactElement, useEffect } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { withStyles, Theme, createStyles } from '@material-ui/core/styles'
|
import TabsContainer from './TabsContainer'
|
||||||
import { Tabs, Tab, Box, Typography } from '@material-ui/core'
|
|
||||||
import CodeBlock from './CodeBlock'
|
import CodeBlock from './CodeBlock'
|
||||||
|
import { Context } from '../providers/Platform'
|
||||||
interface TabPanelProps {
|
|
||||||
children?: React.ReactNode
|
|
||||||
index: number
|
|
||||||
value: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
linux: string
|
linux: string
|
||||||
@@ -15,133 +9,23 @@ interface Props {
|
|||||||
showLineNumbers?: boolean
|
showLineNumbers?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function a11yProps(index: number) {
|
|
||||||
return {
|
|
||||||
id: `simple-tab-${index}`,
|
|
||||||
'aria-controls': `simple-tabpanel-${index}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOS() {
|
|
||||||
const userAgent = window.navigator.userAgent
|
|
||||||
const platform = window.navigator.platform
|
|
||||||
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
|
|
||||||
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
|
|
||||||
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
|
|
||||||
|
|
||||||
if (macosPlatforms.includes(platform)) return 'macOS'
|
|
||||||
|
|
||||||
if (iosPlatforms.includes(platform)) return 'iOS'
|
|
||||||
|
|
||||||
if (windowsPlatforms.includes(platform)) return 'windows'
|
|
||||||
|
|
||||||
if (/Android/.test(userAgent)) return 'android'
|
|
||||||
|
|
||||||
if (/Linux/.test(platform)) return 'linux'
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CodeBlockTabs(props: Props): ReactElement {
|
export default function CodeBlockTabs(props: Props): ReactElement {
|
||||||
const [value, setValue] = React.useState(0)
|
const { platform, setPlatform } = useContext(Context)
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
|
|
||||||
setValue(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const os = getOS()
|
|
||||||
|
|
||||||
if (os === 'windows') {
|
|
||||||
setValue(0)
|
|
||||||
} else if (os === 'linux') {
|
|
||||||
setValue(0)
|
|
||||||
} else if (os === 'macOS') {
|
|
||||||
setValue(1)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
function TabPanel(props: TabPanelProps) {
|
|
||||||
const { children, value, index, ...other } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="tabpanel"
|
|
||||||
hidden={value !== index}
|
|
||||||
id={`simple-tabpanel-${index}`}
|
|
||||||
aria-labelledby={`simple-tab-${index}`}
|
|
||||||
{...other}
|
|
||||||
>
|
|
||||||
{value === index && (
|
|
||||||
<Box style={{ marginTop: '-12px' }}>
|
|
||||||
<Typography component="div">{children}</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const AntTabs = withStyles({
|
|
||||||
root: {
|
|
||||||
borderBottom: '1px solid #e8e8e8',
|
|
||||||
},
|
|
||||||
indicator: {
|
|
||||||
backgroundColor: '#3f51b5',
|
|
||||||
},
|
|
||||||
})(Tabs)
|
|
||||||
|
|
||||||
interface StyledTabProps {
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const AntTab = withStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
textTransform: 'none',
|
|
||||||
minWidth: 72,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
fontWeight: theme.typography.fontWeightRegular,
|
|
||||||
marginRight: theme.spacing(4),
|
|
||||||
fontFamily: [
|
|
||||||
'-apple-system',
|
|
||||||
'BlinkMacSystemFont',
|
|
||||||
'"Segoe UI"',
|
|
||||||
'Roboto',
|
|
||||||
'"Helvetica Neue"',
|
|
||||||
'Arial',
|
|
||||||
'sans-serif',
|
|
||||||
'"Apple Color Emoji"',
|
|
||||||
'"Segoe UI Emoji"',
|
|
||||||
'"Segoe UI Symbol"',
|
|
||||||
].join(','),
|
|
||||||
'&:hover': {
|
|
||||||
color: '#3f51b5',
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
'&$selected': {
|
|
||||||
color: '#3f51b5',
|
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
|
||||||
},
|
|
||||||
'&:focus': {
|
|
||||||
color: '#3f51b5',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
selected: {},
|
|
||||||
}),
|
|
||||||
)((props: StyledTabProps) => <Tab disableRipple {...props} />)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<TabsContainer
|
||||||
<AntTabs style={{ marginTop: '12px' }} value={value} onChange={handleChange} aria-label="ant example">
|
index={platform}
|
||||||
<AntTab label="Linux" {...a11yProps(0)} />
|
indexChanged={setPlatform}
|
||||||
<AntTab label="MacOS" {...a11yProps(1)} />
|
values={[
|
||||||
</AntTabs>
|
{
|
||||||
<TabPanel value={value} index={0}>
|
label: 'Linux',
|
||||||
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />
|
component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />,
|
||||||
</TabPanel>
|
},
|
||||||
<TabPanel value={value} index={1}>
|
{
|
||||||
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />
|
label: 'macOS',
|
||||||
</TabPanel>
|
component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />,
|
||||||
</div>
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ function TabPanel(props: TabPanelProps) {
|
|||||||
const { children, value, index, ...other } = props
|
const { children, value, index, ...other } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div role="tabpanel" hidden={value !== index} {...other}>
|
||||||
role="tabpanel"
|
|
||||||
hidden={value !== index}
|
|
||||||
id={`simple-tabpanel-${index}`}
|
|
||||||
aria-labelledby={`simple-tab-${index}`}
|
|
||||||
{...other}
|
|
||||||
>
|
|
||||||
{value === index && (
|
{value === index && (
|
||||||
<Box p={3}>
|
<Box p={3}>
|
||||||
<Typography>{children}</Typography>
|
<Typography>{children}</Typography>
|
||||||
@@ -44,25 +38,30 @@ interface TabsValues {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
values: TabsValues[]
|
values: TabsValues[]
|
||||||
|
index?: number
|
||||||
|
indexChanged?: (index: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SimpleTabs({ values }: Props): ReactElement {
|
export default function SimpleTabs({ values, index, indexChanged }: Props): ReactElement {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [value, setValue] = React.useState<number>(0)
|
const [value, setValue] = React.useState<number>(index || 0)
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => {
|
const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => {
|
||||||
setValue(newValue)
|
if (indexChanged) indexChanged(newValue)
|
||||||
|
else setValue(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const v = index !== undefined ? index : value
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example">
|
<Tabs value={v} onChange={handleChange}>
|
||||||
{values.map(({ label }, index) => (
|
{values.map(({ label }, idx) => (
|
||||||
<Tab key={index} label={label} />
|
<Tab key={idx} label={label} />
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{values.map(({ component }, index) => (
|
{values.map(({ component }, idx) => (
|
||||||
<TabPanel key={index} value={value} index={index}>
|
<TabPanel key={idx} value={v} index={idx}>
|
||||||
{component}
|
{component}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
// These need to be numeric values as they are used as indexes in the TabsContainer
|
||||||
|
export enum Platforms {
|
||||||
|
macOS = 0,
|
||||||
|
Linux,
|
||||||
|
Windows,
|
||||||
|
iOS,
|
||||||
|
Android,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SupportedPlatforms {
|
||||||
|
macOS = Platforms.macOS,
|
||||||
|
Linux = Platforms.Windows,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContextInterface {
|
||||||
|
platform: SupportedPlatforms
|
||||||
|
setPlatform: (platform: SupportedPlatforms) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues: ContextInterface = {
|
||||||
|
platform: SupportedPlatforms.macOS,
|
||||||
|
setPlatform: () => {}, // eslint-disable-line
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Context = createContext<ContextInterface>(initialValues)
|
||||||
|
export const Consumer = Context.Consumer
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactChild
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSupportedPlatform(platform: unknown): platform is SupportedPlatforms {
|
||||||
|
return Object.keys(SupportedPlatforms).includes(platform as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOS(): Platforms | null {
|
||||||
|
const userAgent = window.navigator.userAgent
|
||||||
|
const platform = window.navigator.platform
|
||||||
|
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
|
||||||
|
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
|
||||||
|
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
|
||||||
|
|
||||||
|
if (macosPlatforms.includes(platform)) return Platforms.macOS
|
||||||
|
|
||||||
|
if (iosPlatforms.includes(platform)) return Platforms.iOS
|
||||||
|
|
||||||
|
if (windowsPlatforms.includes(platform)) return Platforms.Windows
|
||||||
|
|
||||||
|
if (/Android/.test(userAgent)) return Platforms.Android
|
||||||
|
|
||||||
|
if (/Linux/.test(platform)) return Platforms.Linux
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Provider({ children }: Props): ReactElement {
|
||||||
|
const [platform, setPlatform] = useState<SupportedPlatforms>(SupportedPlatforms.Linux)
|
||||||
|
|
||||||
|
// This is in useEffect as it really just needs to run once and not on each re-render
|
||||||
|
useEffect(() => {
|
||||||
|
const os = getOS()
|
||||||
|
|
||||||
|
setPlatform(isSupportedPlatform(os) ? os : SupportedPlatforms.Linux)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <Context.Provider value={{ platform, setPlatform }}>{children}</Context.Provider>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user