Upgrading redux; Refactoring tabs; Adding servers index
This commit is contained in:
@ -7,7 +7,7 @@ export default class TextField extends React.Component {
|
||||
|
||||
this.state = {
|
||||
in_focus: false,
|
||||
value: (this.props.value ? this.props.value : ''),
|
||||
value: props.value || '',
|
||||
};
|
||||
}
|
||||
|
||||
@ -20,31 +20,40 @@ export default class TextField extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
handleChange = (e) => {
|
||||
this.setState({ value: e.target.value });
|
||||
}
|
||||
|
||||
handleFocus(e) {
|
||||
handleFocus = () => {
|
||||
this.setState({ in_focus: true });
|
||||
}
|
||||
|
||||
handleBlur(e) {
|
||||
handleBlur = () => {
|
||||
const { value, onChange } = this.props;
|
||||
const { value: stateValue } = this.state;
|
||||
this.setState({ in_focus: false });
|
||||
if (this.state.value !== this.props.value) {
|
||||
this.props.onChange(this.state.value);
|
||||
if (stateValue !== value) {
|
||||
onChange(stateValue);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
render = () => {
|
||||
const {
|
||||
className,
|
||||
type = 'text',
|
||||
placeholder,
|
||||
} = this.props;
|
||||
const { value } = this.state;
|
||||
|
||||
return (
|
||||
<input
|
||||
className={this.props.className ? this.props.className : ''}
|
||||
type={this.props.type ? this.props.type : 'text'}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onFocus={(e) => this.handleFocus(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
value={this.state.value}
|
||||
placeholder={this.props.placeholder ? this.props.placeholder : null}
|
||||
className={className}
|
||||
type={type}
|
||||
onChange={this.handleChange}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -66,49 +66,49 @@ class Hotkeys extends React.Component {
|
||||
prevent = true;
|
||||
break;
|
||||
|
||||
case 'escape':
|
||||
if (dragging) {
|
||||
uiActions.dragEnd();
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Dragging' });
|
||||
} else if (modal) {
|
||||
window.history.back();
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Modal' });
|
||||
case 's': // Stop
|
||||
mopidyActions.stop();
|
||||
uiActions.createNotification({ content: 'stop', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Stop' });
|
||||
break;
|
||||
|
||||
case 'b': // Seek backwards 30 sec
|
||||
var new_position = play_time_position - 30000;
|
||||
if (new_position < 0) {
|
||||
new_position = 0;
|
||||
}
|
||||
mopidyActions.setTimePosition(new_position);
|
||||
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Rewind' });
|
||||
break;
|
||||
|
||||
case 's':
|
||||
history.push('/search');
|
||||
case 'f': // Seek forwards 30 sec
|
||||
mopidyActions.setTimePosition(play_time_position + 30000);
|
||||
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Search' });
|
||||
break;
|
||||
|
||||
case 'q':
|
||||
history.push('/queue');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Queue' });
|
||||
break;
|
||||
|
||||
case 'k':
|
||||
history.push('/kiosk-mode');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Kiosk mode' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Fastforward' });
|
||||
break;
|
||||
|
||||
case ',':
|
||||
window.history.back();
|
||||
case '<': // Previous track
|
||||
mopidyActions.previous();
|
||||
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Back' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Previous' });
|
||||
break;
|
||||
|
||||
case '.':
|
||||
window.history.forward();
|
||||
case '>': // Next track
|
||||
mopidyActions.next();
|
||||
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Forward' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Next' });
|
||||
break;
|
||||
|
||||
case '=':
|
||||
case '+': // Volume up
|
||||
if (volume !== 'false') {
|
||||
volume += 5;
|
||||
if (volume > 100) {
|
||||
@ -124,7 +124,7 @@ class Hotkeys extends React.Component {
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume up' });
|
||||
break;
|
||||
|
||||
case '-':
|
||||
case '-': // Volume down
|
||||
if (volume !== 'false') {
|
||||
volume -= 5;
|
||||
if (volume < 0) {
|
||||
@ -140,7 +140,7 @@ class Hotkeys extends React.Component {
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Volume down' });
|
||||
break;
|
||||
|
||||
case '0':
|
||||
case '0': // Mute
|
||||
if (mute) {
|
||||
mopidyActions.setMute(false);
|
||||
uiActions.createNotification({ content: 'volume_up', type: 'shortcut' });
|
||||
@ -153,36 +153,34 @@ class Hotkeys extends React.Component {
|
||||
prevent = true;
|
||||
break;
|
||||
|
||||
case ';':
|
||||
var new_position = play_time_position - 30000;
|
||||
if (new_position < 0) {
|
||||
new_position = 0;
|
||||
case 'escape': // Cancel current action/context
|
||||
if (dragging) {
|
||||
uiActions.dragEnd();
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Dragging' });
|
||||
} else if (modal) {
|
||||
window.history.back();
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Modal' });
|
||||
}
|
||||
mopidyActions.setTimePosition(new_position);
|
||||
uiActions.createNotification({ content: 'fast_rewind', type: 'shortcut' });
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Rewind' });
|
||||
break;
|
||||
|
||||
case "'":
|
||||
mopidyActions.setTimePosition(play_time_position + 30000);
|
||||
uiActions.createNotification({ content: 'fast_forward', type: 'shortcut' });
|
||||
case '1': // Navigation: Queue
|
||||
history.push('/queue');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Fastforward' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Queue' });
|
||||
break;
|
||||
|
||||
case '[':
|
||||
mopidyActions.previous();
|
||||
uiActions.createNotification({ content: 'skip_previous', type: 'shortcut' });
|
||||
case '2': // Navigation: Search
|
||||
history.push('/search');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Previous' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Search' });
|
||||
break;
|
||||
|
||||
case ']':
|
||||
mopidyActions.next();
|
||||
uiActions.createNotification({ content: 'skip_next', type: 'shortcut' });
|
||||
case '3': // Navigation: Kiosk mode
|
||||
history.push('/kiosk-mode');
|
||||
prevent = true;
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Next' });
|
||||
if (allow_reporting) ReactGA.event({ category: 'Hotkey', action: key, label: 'Kiosk mode' });
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
95
src/js/components/Servers.js
Executable file
95
src/js/components/Servers.js
Executable file
@ -0,0 +1,95 @@
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useStore, useDispatch } from 'react-redux'
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Link from './Link';
|
||||
import Icon from './Icon';
|
||||
import URILink from './URILink';
|
||||
import TextField from './Fields/TextField';
|
||||
import { indexToArray } from '../util/arrays';
|
||||
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
import * as coreActions from '../services/core/actions';
|
||||
import * as mopidyActions from '../services/mopidy/actions';
|
||||
|
||||
const Servers = ({
|
||||
match: { params: { id: serverId } },
|
||||
history,
|
||||
}) => {
|
||||
const store = useStore();
|
||||
const dispatch = useDispatch();
|
||||
const { mopidy: { servers } } = store.getState();
|
||||
|
||||
const renderMenu = () => (
|
||||
<div className="sub-tabs__menu" id="servers-menu">
|
||||
<div className="menu__inner">
|
||||
{indexToArray(servers).map((server) => (
|
||||
<Link
|
||||
history={history}
|
||||
className="menu-item"
|
||||
activeClassName="menu-item--active"
|
||||
to={`/settings/servers/${server.id}`}
|
||||
scrollTo="#servers-menu"
|
||||
key={server.id}
|
||||
>
|
||||
<div className="menu-item__inner">
|
||||
<div className="menu-item__title">
|
||||
{server.name}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderServer = () => {
|
||||
if (!serverId) return null;
|
||||
const server = servers[serverId];
|
||||
if (!server) return null;
|
||||
|
||||
return (
|
||||
<div className="sub-tabs__content">
|
||||
<label className="field">
|
||||
<div className="name">Name</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
type="text"
|
||||
value={server.name}
|
||||
onChange={value => dispatch(mopidyActions.updateServer({ id: server.id, name: value }))}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label className="field">
|
||||
<div className="name">Host</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
value={server.host}
|
||||
onChange={value => updateServer({ id: server.id, host: value })}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label className="field">
|
||||
<div className="name">Port</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
type="text"
|
||||
value={server.port}
|
||||
onChange={value => updateServer({ id: server.id, port: value })}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sub-tabs sub-tabs--servers">
|
||||
{renderMenu()}
|
||||
{renderServer()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Servers;
|
||||
@ -7,6 +7,7 @@ import Link from './Link';
|
||||
import Thumbnail from './Thumbnail';
|
||||
import Icon from './Icon';
|
||||
import URILink from './URILink';
|
||||
import TextField from './Fields/TextField';
|
||||
import SpotifyAuthenticationFrame from './Fields/SpotifyAuthenticationFrame';
|
||||
import LastfmAuthenticationFrame from './Fields/LastfmAuthenticationFrame';
|
||||
import GeniusAuthenticationFrame from './Fields/GeniusAuthenticationFrame';
|
||||
@ -109,11 +110,8 @@ class Services extends React.Component {
|
||||
<label className="field">
|
||||
<div className="name">Country</div>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => this.setState({ country: e.target.value })}
|
||||
onFocus={(e) => this.setState({ input_in_focus: 'country' })}
|
||||
onBlur={(e) => this.props.spotifyActions.set({ country: e.target.value })}
|
||||
<TextField
|
||||
onChange={value => this.props.spotifyActions.set({ country: value })}
|
||||
value={this.state.country}
|
||||
/>
|
||||
<div className="description">
|
||||
@ -322,9 +320,9 @@ class Services extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="menu" id="services-menu">
|
||||
<div className="sub-tabs__menu menu" id="services-menu">
|
||||
<div className="menu__inner">
|
||||
<Link history={this.props.history} className="menu-item menu-item--spotify" activeClassName="menu-item--active" to="/settings/spotify" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--spotify" activeClassName="menu-item--active" to="/settings/services/spotify" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
{spotify_icon}
|
||||
<div className="menu-item__title">
|
||||
@ -333,7 +331,7 @@ class Services extends React.Component {
|
||||
{this.props.spotify.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Read-only</span>}
|
||||
</div>
|
||||
</Link>
|
||||
<Link history={this.props.history} className="menu-item menu-item--lastfm" activeClassName="menu-item--active" to="/settings/lastfm" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--lastfm" activeClassName="menu-item--active" to="/settings/services/lastfm" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
{lastfm_icon}
|
||||
<div className="menu-item__title">
|
||||
@ -342,7 +340,7 @@ class Services extends React.Component {
|
||||
{this.props.lastfm.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Read-only</span>}
|
||||
</div>
|
||||
</Link>
|
||||
<Link history={this.props.history} className="menu-item menu-item--genius" activeClassName="menu-item--active" to="/settings/genius" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--genius" activeClassName="menu-item--active" to="/settings/services/genius" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
{genius_icon}
|
||||
<div className="menu-item__title">
|
||||
@ -351,7 +349,7 @@ class Services extends React.Component {
|
||||
{this.props.genius.authorization ? <span className="status green-text">Authorized</span> : <span className="status mid_grey-text">Unauthorized</span>}
|
||||
</div>
|
||||
</Link>
|
||||
<Link history={this.props.history} className="menu-item menu-item--snapcast" activeClassName="menu-item--active" to="/settings/snapcast" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--snapcast" activeClassName="menu-item--active" to="/settings/services/snapcast" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
<Icon className="menu-item__icon" name="devices" />
|
||||
<div className="menu-item__title">
|
||||
@ -368,7 +366,7 @@ class Services extends React.Component {
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<Link history={this.props.history} className="menu-item menu-item--icecast" activeClassName="menu-item--active" to="/settings/icecast" scrollTo="#services-menu">
|
||||
<Link history={this.props.history} className="menu-item menu-item--icecast" activeClassName="menu-item--active" to="/settings/services/icecast" scrollTo="#services-menu">
|
||||
<div className="menu-item__inner">
|
||||
<Icon className="menu-item__icon" name="wifi_tethering" />
|
||||
<div className="menu-item__title">
|
||||
@ -386,15 +384,15 @@ class Services extends React.Component {
|
||||
const { match } = this.props;
|
||||
switch (match.params.service) {
|
||||
case 'spotify':
|
||||
return <div className="service">{this.renderSpotify()}</div>;
|
||||
return <div className="sub-tabs__content">{this.renderSpotify()}</div>;
|
||||
case 'lastfm':
|
||||
return <div className="service">{this.renderLastfm()}</div>;
|
||||
return <div className="sub-tabs__content">{this.renderLastfm()}</div>;
|
||||
case 'genius':
|
||||
return <div className="service">{this.renderGenius()}</div>;
|
||||
return <div className="sub-tabs__content">{this.renderGenius()}</div>;
|
||||
case 'icecast':
|
||||
return <div className="service">{this.renderIcecast()}</div>;
|
||||
return <div className="sub-tabs__content">{this.renderIcecast()}</div>;
|
||||
case 'snapcast':
|
||||
return <div className="service">{<Snapcast match={this.props.match} />}</div>;
|
||||
return <div className="sub-tabs__content">{<Snapcast match={this.props.match} />}</div>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -402,7 +400,7 @@ class Services extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="services">
|
||||
<div className="sub-tabs sub-tabs--services">
|
||||
{this.renderMenu()}
|
||||
{this.renderService()}
|
||||
</div>
|
||||
|
||||
@ -137,7 +137,7 @@ const SnapcastGroups = (props) => {
|
||||
activeClassName="menu-item--active"
|
||||
key={group.id}
|
||||
history={history}
|
||||
to={`/settings/snapcast/${group.id}`}
|
||||
to={`/settings/services/snapcast/${group.id}`}
|
||||
scrollTo="#services-snapcast-groups"
|
||||
>
|
||||
<div className="snapcast__groups__menu-item__inner menu-item__inner">
|
||||
@ -154,8 +154,8 @@ const SnapcastGroups = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="snapcast__groups" id="services-snapcast-groups">
|
||||
<div className="snapcast__groups__menu menu">
|
||||
<div className="sub-tabs snapcast__groups" id="services-snapcast-groups">
|
||||
<div className="sub-tabs__menu snapcast__groups__menu menu">
|
||||
<div className="menu__inner">
|
||||
{sortItems(groupsArray, 'name').map((group) => renderMenuItem(group))}
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,13 @@ export function set(data) {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateServer(server) {
|
||||
return {
|
||||
type: 'MOPIDY_UPDATE_SERVER',
|
||||
server,
|
||||
};
|
||||
}
|
||||
|
||||
export function setConnection(data) {
|
||||
return {
|
||||
type: 'MOPIDY_SET_CONNECTION',
|
||||
|
||||
@ -6,6 +6,11 @@ export default function reducer(mopidy = {}, action) {
|
||||
case 'MOPIDY_SET':
|
||||
return { ...mopidy, ...action.data };
|
||||
|
||||
case 'MOPIDY_UPDATE_SERVER':
|
||||
const updated = {};
|
||||
updated[action.server.id] = { ...mopidy.servers[action.server.id], ...action.server };
|
||||
return { ...mopidy, servers: { ...mopidy.servers, ...updated } };
|
||||
|
||||
case 'MOPIDY_CONNECT':
|
||||
case 'MOPIDY_CONNECTING':
|
||||
return {
|
||||
|
||||
@ -61,6 +61,15 @@ let state = {
|
||||
host: window.location.hostname,
|
||||
port: (window.location.port ? window.location.port : (window.location.protocol === 'https:' ? '443' : '80')),
|
||||
ssl: (window.location.protocol === 'https:'),
|
||||
servers: {
|
||||
default: {
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
host: window.location.hostname,
|
||||
port: (window.location.port ? window.location.port : (window.location.protocol === 'https:' ? '443' : '80')),
|
||||
ssl: (window.location.protocol === 'https:')
|
||||
},
|
||||
},
|
||||
mute: false,
|
||||
volume: 0,
|
||||
progress: 0,
|
||||
|
||||
@ -11,6 +11,7 @@ import TextField from '../components/Fields/TextField';
|
||||
import Header from '../components/Header';
|
||||
import Icon from '../components/Icon';
|
||||
import Services from '../components/Services';
|
||||
import Servers from '../components/Servers';
|
||||
import * as coreActions from '../services/core/actions';
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
import * as pusherActions from '../services/pusher/actions';
|
||||
@ -205,34 +206,14 @@ class Settings extends React.Component {
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<form onSubmit={(e) => this.setConfig(e)}>
|
||||
<label className="field">
|
||||
<div className="name">Host</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
value={mopidy.host}
|
||||
onChange={value => mopidyActions.setConnection({ host: value })}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label className="field">
|
||||
<div className="name">Port</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
type="text"
|
||||
value={mopidy.port}
|
||||
onChange={value => mopidyActions.setConnection({ port: value })}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</form>
|
||||
<Route path="/settings/:server?/:id?" component={Servers} />
|
||||
|
||||
<h4 className="underline">
|
||||
Services
|
||||
<a name="services" />
|
||||
</h4>
|
||||
|
||||
<Route path="/settings/:service?/:id?" component={Services} />
|
||||
<Route path="/settings/:services?/:service?/:id?" component={Services} />
|
||||
|
||||
<h4 className="underline">
|
||||
Interface
|
||||
|
||||
@ -27,9 +27,9 @@
|
||||
@import 'components/filter-field';
|
||||
@import 'components/sources-priority-field';
|
||||
@import 'components/sub-views';
|
||||
@import 'components/sub-tabs';
|
||||
@import 'components/debug';
|
||||
@import 'components/tooltip';
|
||||
@import 'components/services';
|
||||
@import 'components/snapcast';
|
||||
@import 'components/popularity';
|
||||
@import 'components/colour-field';
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
|
||||
.services {
|
||||
.sub-tabs {
|
||||
|
||||
.menu {
|
||||
&__menu {
|
||||
@include clearfix();
|
||||
|
||||
&__inner {
|
||||
.menu__inner {
|
||||
display: flex;
|
||||
margin: 0 0 0 -5px;
|
||||
}
|
||||
@ -89,7 +89,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.service {
|
||||
&__content {
|
||||
padding: 20px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: lighten(colour(dark_grey), 4%);
|
||||
@ -107,7 +107,7 @@
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
|
||||
.menu {
|
||||
&__menu {
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar{
|
||||
@ -135,7 +135,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.service {
|
||||
&__content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user