Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Branding options using runtime config, closes #1859 #2538

Draft
wants to merge 5 commits into
base: default
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Create a Web client middleware for use with express-style server libraries.
**Parameters**

* `options`
* `options.apiBase` - Base URL to the mount point of the
* `options.apiUrl` - Base URL to the mount point of the
[üWave Web API][u-wave-core] to talk to.
Defaults to `/api`, but it's recommended to set this explicitly.
* `options.emoji` - An object describing the custom emoji that will be
Expand Down Expand Up @@ -49,7 +49,7 @@ app.listen(6041);

app.use('/', createWebClient({
// Use nginx to send this traffic to the API server.
apiBase: 'https://example.com/api',
apiUrl: 'https://example.com/api',
recaptcha: { key: 'my ReCaptcha site key' },
}));
```
Expand Down
60 changes: 60 additions & 0 deletions src/admin/components/SchemaForm/AssetField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useAsyncCallback } from 'react-async-hook';
import Typography from '@mui/material/Typography';
import FormHelperText from '@mui/material/FormHelperText';
import Button from '@mui/material/Button';
import FileIcon from '@mui/icons-material/FileUpload';
import TextField from '../../../components/Form/TextField';

const {
useRef,
} = React;

function AssetField({
className,
schema,
value,
onChange,
}) {
const fileInput = useRef(null);
const handleSelect = useAsyncCallback(async (event) => {
const file = event.target.files[0];
const body = new FormData();
body.append('file', file);

const res = await fetch('/api/server/config/asset/u-wave:base.logo', {
method: 'put',
body,
});
const json = await res.json();
const { data } = json;
onChange(data.path);
}, [onChange]);

return (
<div className={className} style={{ marginBottom: '8px' }}>
{schema.title && <Typography gutterBottom>{schema.title}</Typography>}
<input ref={fileInput} type="file" onChange={handleSelect.execute} style={{ display: 'none' }} />
<div style={{ display: 'flex', gap: '.5rem' }}>
<TextField type="text" value={value} disabled icon={<FileIcon />} />
<Button
variant="contained"
onClick={() => fileInput.current.click()}
>
Select
</Button>
</div>
{schema.description && <FormHelperText>{schema.description}</FormHelperText>}
</div>
);
}

AssetField.propTypes = {
className: PropTypes.string,
schema: PropTypes.object.isRequired,
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
};

export default AssetField;
4 changes: 4 additions & 0 deletions src/admin/components/SchemaForm/Field.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ function getControlName(schema) {
return 'enum';
}

if (schema.type === 'string' && schema.format === 'asset') {
return 'asset';
}

return schema.type;
}

Expand Down
2 changes: 2 additions & 0 deletions src/admin/components/SchemaForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import NumberField from './NumberField';
import ObjectField, { ObjectProperties } from './ObjectField';
import StringField from './StringField';
import MarkdownField from './MarkdownField';
import AssetField from './AssetField';
import EnumField from './EnumField';

const controls = new Map([
Expand All @@ -18,6 +19,7 @@ const controls = new Map([
['string', StringField],
['enum', EnumField],
['markdown', MarkdownField],
['asset', AssetField],
]);

function SchemaForm({
Expand Down
36 changes: 20 additions & 16 deletions src/components/HeaderBar/AppTitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import IconButton from '@mui/material/IconButton';
import AboutIcon from '@mui/icons-material/ArrowDropDown';

const logo = new URL('../../../assets/img/logo-white.png', import.meta.url);
const defaultLogo = new URL('../../../assets/img/logo-white.png', import.meta.url);

// We use `logo.pathname` to ensure that the prerendered loading screen
// does not include a `file://` prefix. This should still work fine in browsers
Expand All @@ -17,28 +17,32 @@ function pathname(url) {
return url.pathname;
}

const AppTitle = ({
function AppTitle({
className,
children,
logo = defaultLogo,
onClick,
}) => (
<div className={cx('AppTitle', className)}>
<h1 className="AppTitle-logo">
<img
className="AppTitle-logoImage"
alt={children}
src={pathname(logo)}
/>
</h1>
<IconButton className="AppTitle-button" onClick={onClick}>
<AboutIcon />
</IconButton>
</div>
);
}) {
return (
<div className={cx('AppTitle', className)}>
<h1 className="AppTitle-logo">
<img
className="AppTitle-logoImage"
alt={children}
src={pathname(new URL(logo))}
/>
</h1>
<IconButton className="AppTitle-button" onClick={onClick}>
<AboutIcon />
</IconButton>
</div>
);
}

AppTitle.propTypes = {
className: PropTypes.string,
children: PropTypes.string.isRequired,
logo: PropTypes.string,
onClick: PropTypes.func.isRequired,
};

Expand Down
3 changes: 3 additions & 0 deletions src/components/HeaderBar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import CurrentDJ from './CurrentDJ';
const HeaderBar = ({
className,
title,
logo,
dj,
media,
mediaStartTime,
Expand All @@ -29,6 +30,7 @@ const HeaderBar = ({
>
<AppTitle
className="HeaderBar-title"
logo={logo}
onClick={onToggleAboutOverlay}
>
{title}
Expand Down Expand Up @@ -62,6 +64,7 @@ const HeaderBar = ({
HeaderBar.propTypes = {
className: PropTypes.string,
title: PropTypes.string,
logo: PropTypes.string,

dj: PropTypes.object,
media: PropTypes.object,
Expand Down
3 changes: 3 additions & 0 deletions src/containers/HeaderBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { setVolume, mute, unmute } from '../actions/PlaybackActionCreators';
import { toggleRoomHistory, toggleAbout } from '../actions/OverlayActionCreators';
import { djSelector, mediaSelector, startTimeSelector } from '../selectors/boothSelectors';
import { volumeSelector, isMutedSelector } from '../selectors/settingSelectors';
import { serverNameSelector, serverLogoSelector } from '../selectors/configSelectors';
import HeaderBar from '../components/HeaderBar';

const mapStateToProps = createStructuredSelector({
title: serverNameSelector,
logo: serverLogoSelector,
mediaStartTime: startTimeSelector,
media: mediaSelector,
dj: djSelector,
Expand Down
17 changes: 11 additions & 6 deletions src/reducers/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ const initialState = {};
export default function reduce(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case INIT_STATE:
case INIT_STATE: {
const patch = {};
if (payload.roles) {
return {
...state,
roles: payload.roles,
};
patch.roles = payload.roles;
}
return state;
if (payload.config?.['u-wave:base']) {
Object.assign(patch, payload.config['u-wave:base']);
}
if (Object.keys(patch).length === 0) {
return state;
}
return { ...state, ...patch };
}
default:
return state;
}
Expand Down
11 changes: 11 additions & 0 deletions src/selectors/configSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import defaultEmoji from '../utils/emojiShortcodes';

export const configSelector = (state) => state.config;

export const serverNameSelector = createSelector(configSelector, (config) => {
return config.name ?? 'üWave';
});
export const serverLogoSelector = createSelector(configSelector, (config) => {
if (config.logo) {
const ASSET_BASE_URL = new URL('/assets/', window.location.href);
return new URL(config.logo.replace(/^\//, ''), ASSET_BASE_URL);
}
return undefined;
});

export const requestOptionsSelector = createSelector(
configSelector,
(config) => {
Expand Down
4 changes: 4 additions & 0 deletions tasks/serve.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const socketUrl = Object.assign(new URL(serverUrl.href), { protocol: 'ws:' }).hr
app.use(apiUrl, createProxyMiddleware({
target: serverUrl.href,
}));
app.use('/assets', createProxyMiddleware({
target: serverUrl.href,
}));


if (watch) {
const compiler = webpack(wpConfig);
Expand Down