reactify add guild overlay

This commit is contained in:
Michael Peters 2021-12-08 23:31:09 -06:00
parent 392a8cf4bc
commit f57ef4848f
10 changed files with 349 additions and 270 deletions

View File

@ -18,12 +18,14 @@ interface DisplayProps {
saving: boolean;
saveFailed: boolean;
errorMessage: string | null;
infoMessage: string | null;
}
const Display: FC<DisplayProps> = (props: DisplayProps) => {
const { children, changes, resetChanges, saveChanges, saving, saveFailed, errorMessage } = props;
const { children, changes, resetChanges, saveChanges, saving, saveFailed, errorMessage, infoMessage } = props;
const [ saveButtonShaking, setSaveButtonShaking ] = useState<boolean>(false);
const [ dismissedInfoMessage, setDismissedInfoMessage ] = useState<string | null>(null);
useEffect(() => {
(async () => {
@ -43,6 +45,10 @@ const Display: FC<DisplayProps> = (props: DisplayProps) => {
}
}, [ saving, saveFailed ]);
const dismissInfoMessage = () => {
setDismissedInfoMessage(infoMessage);
}
const popup = useMemo(() => {
if (errorMessage) {
return (
@ -50,9 +56,15 @@ const Display: FC<DisplayProps> = (props: DisplayProps) => {
<Button type={ButtonType.PERDU} onClick={resetChanges}>Reset</Button>
</DisplayPopup>
);
} else if (infoMessage && infoMessage !== dismissedInfoMessage) {
return (
<DisplayPopup tip={infoMessage}>
<Button type={ButtonType.PERDU} onClick={dismissInfoMessage}>X</Button>
</DisplayPopup>
);
} else if (changes) {
return (
<DisplayPopup tip={'You have unsaved changes'}>
<DisplayPopup tip="You have unsaved changes">
<Button type={ButtonType.PERDU} onClick={resetChanges}>Reset</Button>
<Button type={ButtonType.POSITIVE} onClick={saveChanges} shaking={saveButtonShaking}>{changesButtonText}</Button>
</DisplayPopup>

View File

@ -18,11 +18,12 @@ interface ImageEditInputProps {
value: Buffer | null;
setValue: React.Dispatch<React.SetStateAction<Buffer | null>>;
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>;
setValid: React.Dispatch<React.SetStateAction<boolean>>;
setMessage: React.Dispatch<React.SetStateAction<string | null>>;
}
const ImageEditInput: FC<ImageEditInputProps> = (props: ImageEditInputProps) => {
const { alt, maxSize, value, setValue, setErrorMessage } = props;
const { alt, maxSize, value, setValue, setValid, setMessage } = props;
const acceptedExtTypes = [ 'png', 'jpg', 'jpeg' ];
@ -36,13 +37,16 @@ const ImageEditInput: FC<ImageEditInputProps> = (props: ImageEditInputProps) =>
try {
const src = await ElementsUtil.getImageBufferSrc(value);
setImgSrc(src);
setValid(true);
} catch (e: unknown) {
LOG.error('unable to get image buffer src', e);
setImgSrc('./img/error.png');
setErrorMessage('Unable to get image src');
setValid(false);
setMessage('Unable to get image src');
}
} else {
setImgSrc('./img/loading.svg');
setValid(false);
}
})();
}, [ value ]);
@ -63,17 +67,17 @@ const ImageEditInput: FC<ImageEditInputProps> = (props: ImageEditInputProps) =>
if (file.size > maxSize) {
e.target.value = '';
LOG.debug('image too large');
setErrorMessage(`Image too large. (${ElementsUtil.humanSize(file.size)}>${ElementsUtil.humanSize(maxSize)})`);
setMessage(`Image too large. (${ElementsUtil.humanSize(file.size)}>${ElementsUtil.humanSize(maxSize)})`);
return;
}
const buff = Buffer.from(await file.arrayBuffer());
const typeResult = await FileType.fromBuffer(buff);
if (!typeResult || !acceptedExtTypes.includes(typeResult.ext)) {
e.target.value = '';
setErrorMessage(`Invalid image type. Accepted types: ${acceptedExtTypes.join(', ')}`);
setMessage(`Invalid image type. Accepted types: ${acceptedExtTypes.join(', ')}`);
return;
}
setErrorMessage(null);
setMessage(null);
setValue(buff);
}

View File

@ -3,27 +3,51 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useMemo, useState } from 'react';
import React, { FC, useEffect, useMemo, useState } from 'react';
export interface TextInputProps {
label?: string;
label: string;
placeholder?: string;
noLabel?: boolean;
allowEmpty?: boolean;
maxLength: number;
value: string;
setValue: React.Dispatch<React.SetStateAction<string>>;
setValid: React.Dispatch<React.SetStateAction<boolean>>;
setMessage: React.Dispatch<React.SetStateAction<string | null>>;
onEnterKeyDown?: () => void;
}
const TextInput: FC<TextInputProps> = (props: TextInputProps) => {
const { label, placeholder, value, setValue, onEnterKeyDown } = props;
const { label, placeholder, noLabel, allowEmpty, maxLength, value, setValue, setValid, setMessage, onEnterKeyDown } = props;
useEffect(() => {
if (maxLength !== undefined && value.length > maxLength) {
setValid(false);
setMessage(`${label} is too long (${value.length}>${maxLength})`);
return;
}
if (!allowEmpty && value.length === 0) {
setValid(false);
setMessage(`${label} is empty`);
return;
}
setValid(true);
setMessage(null);
}, [ value ]);
const labelElement = useMemo(() => {
return label && <div className="label">{label}</div>
return label && !noLabel && <div className="label">{label}</div>
}, [ label ]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
const value = e.target.value;
setValue(value);
}
const handleKeyDown = (e: React.KeyboardEvent) => {
@ -37,7 +61,7 @@ const TextInput: FC<TextInputProps> = (props: TextInputProps) => {
{labelElement}
<input type="text" placeholder={placeholder} onChange={handleChange} onKeyDown={handleKeyDown} value={value} />
</div>
)
);
}
export default TextInput;

View File

@ -0,0 +1,57 @@
import React, { FC, useMemo, useState } from 'react';
import ElementsUtil from '../require/elements-util';
import Button from './button';
// Includes a submit button and error message
export interface SubmitOverlayLowerProps {
buttonMessage: string;
doSubmit: () => Promise<boolean>;
errorMessage: string | null;
}
const SubmitOverlayLower: FC<SubmitOverlayLowerProps> = (props: SubmitOverlayLowerProps) => {
const { buttonMessage, doSubmit, errorMessage } = props;
const [ shaking, setShaking ] = useState<boolean>(false)
const [ submitting, setSubmitting ] = useState<boolean>(false);
const [ submitFailed, setSubmitFailed ] = useState<boolean>(false);
const buttonText = useMemo(() => {
if (submitting) {
return 'Submitting...';
} else if (submitFailed) {
return 'Try Again';
} else {
return buttonMessage;
}
}, [ submitting, submitFailed ]);
const onSubmit = async () => {
setSubmitting(true);
const succeeded = await doSubmit();
setSubmitting(false);
setSubmitFailed(!succeeded);
if (!succeeded) {
await ElementsUtil.delayToggleState(setShaking, 400);
}
}
const isEmpty = useMemo(() => errorMessage === null, [ errorMessage ]);
return (
<div className="lower-react">
<div className={'error-container' + (isEmpty ? ' empty' : '')}>
<div className="error">{errorMessage}</div>
</div>
<div className="buttons">
<Button onClick={onSubmit} shaking={shaking}>{buttonText}</Button>
</div>
</div>
);
}
export default SubmitOverlayLower;

View File

@ -31,7 +31,11 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
const [ iconFailed, setIconFailed ] = useState<boolean>(false);
const [ saveFailed, setSaveFailed ] = useState<boolean>(false);
const [ imageInputErrorMessage, setImageInputErrorMessage ] = useState<string | null>(null);
const [ nameInputValid, setNameInputValid ] = useState<boolean>(false);
const [ nameInputMessage, setNameInputMessage ] = useState<string | null>(null);
const [ iconInputValid, setIconInputValid ] = useState<boolean>(false);
const [ iconInputMessage, setIconInputMessage ] = useState<string | null>(null);
useEffect(() => {
(async () => {
@ -53,21 +57,26 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
const errorMessage = useMemo(() => {
if (iconFailed) {
return 'Unable to load icon';
} else if (imageInputErrorMessage) {
return imageInputErrorMessage;
} else if (iconBuff && iconBuff.length > Globals.MAX_GUILD_ICON_SIZE) {
return `Icon is too large. (${ElementsUtil.humanSize(iconBuff.length)}>${ElementsUtil.humanSize(Globals.MAX_GUILD_ICON_SIZE)}) Try a 512x512 icon.`;
} else if (name.length === 0) {
return 'Name is empty';
} else if (name.length > Globals.MAX_GUILD_NAME_LENGTH) {
return `Name is too long. (${name.length}>${Globals.MAX_GUILD_NAME_LENGTH} characters)`;
} else if (!iconInputValid && iconInputMessage) {
return iconInputMessage;
} else if (!nameInputValid && nameInputMessage) {
return nameInputMessage;
} else {
return null;
}
}, [ iconFailed, imageInputErrorMessage, iconBuff, name ]);
}, [ iconFailed, iconInputValid, iconInputMessage, nameInputValid, nameInputMessage ]);
const infoMessage = useMemo(() => {
if (iconInputValid && iconInputMessage) {
return iconInputMessage;
} else if (nameInputValid && nameInputMessage) {
return nameInputMessage;
} else {
return null;
}
}, [ iconInputValid, iconInputMessage, nameInputValid, nameInputMessage ]);
const resetChanges = () => {
setImageInputErrorMessage(null);
setName(savedName);
setIconBuff(savedIconBuff);
}
@ -112,14 +121,23 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
<Display
changes={changes}
resetChanges={resetChanges} saveChanges={saveChanges}
saving={saving} saveFailed={saveFailed} errorMessage={errorMessage}
saving={saving} saveFailed={saveFailed} errorMessage={errorMessage} infoMessage={infoMessage}
>
<div className="metadata">
<div className="icon">
<ImageEditInput maxSize={Globals.MAX_GUILD_ICON_SIZE} value={iconBuff} setValue={setIconBuff} setErrorMessage={setImageInputErrorMessage} />
<ImageEditInput
maxSize={Globals.MAX_GUILD_ICON_SIZE} value={iconBuff} setValue={setIconBuff}
setValid={setIconInputValid} setMessage={setIconInputMessage}
/>
</div>
<div className="name">
<TextInput label={'Guild Name'} placeholder={savedName} value={name} setValue={setName} onEnterKeyDown={saveChanges} />
<TextInput
label={'Guild Name'} placeholder={savedName}
maxLength={Globals.MAX_GUILD_NAME_LENGTH}
value={name} setValue={setName}
setValid={setNameInputValid} setMessage={setNameInputMessage}
onEnterKeyDown={saveChanges}
/>
</div>
</div>
</Display>

View File

@ -12,6 +12,8 @@ import Q from '../q-module';
import UI from '../ui';
import GuildsManager from '../guilds-manager';
import createErrorMessageOverlay from './overlay-error-message';
import AddGuildOverlay from './overlays/overlay-add-guild';
import React from 'react';
export default function bindAddGuildEvents(document: Document, q: Q, ui: UI, guildsManager: GuildsManager): void {
let choosingFile = false;
@ -52,8 +54,9 @@ export default function bindAddGuildEvents(document: Document, q: Q, ui: UI, gui
LOG.debug('bad guild data:', { addGuildData, fileText })
throw new Error('bad guild data');
}
const overlayElement = createAddGuildOverlay(document, q, ui, guildsManager, addGuildData as IAddGuildData);
document.body.appendChild(overlayElement);
ElementsUtil.presentReactOverlay(document, <AddGuildOverlay guildsManager={guildsManager} addGuildData={addGuildData} />)
// const overlayElement = createAddGuildOverlay(document, q, ui, guildsManager, addGuildData as IAddGuildData);
// document.body.appendChild(overlayElement);
} catch (e: unknown) {
LOG.error('Unable to parse guild data', e);
const errorOverlayElement = createErrorMessageOverlay(document, 'Unable to parse guild file', (e as Error).message);

View File

@ -1,204 +0,0 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import Globals from '../globals';
import BaseElements, { HTMLElementWithRemoveSelf } from './require/base-elements';
import ElementsUtil from './require/elements-util';
import { GuildMetadata } from '../data-types';
import Q from '../q-module';
import CombinedGuild from '../guild-combined';
import React from 'react';
export default function createGuildSettingsOverlay(document: Document, q: Q, guild: CombinedGuild, guildMeta: GuildMetadata): HTMLElementWithRemoveSelf {
const element = BaseElements.createOverlay(document, (
<div className="content display-swapper guild-settings">
<div className="options">
<div className="title">{guildMeta.name}</div>
<div className="choosable chosen">Overview</div>
<div className="choosable">Channels</div>
<div className="choosable">Roles</div>
<div className="choosable">Invites</div>
</div>
<div className="display">
<div className="scroll">
<div className="metadata">
<label className="image-input-label">
<div className="icon">
<img className="guild-icon" src="./img/loading.svg" alt="icon"></img>
<div className="modify"><img src="./img/pencil-icon.png" alt="modify"></img></div>
</div>
<input className="image-input-upload" type="file" accept=".png,.jpg,.jpeg" style={{ display: 'none' }}></input>
</label>
<div className="name">
<div className="label">Guild Name</div>
<input className="guild-name" type="text" placeholder={guildMeta.name}></input>
</div>
</div>
</div>
<div className="popup changes">
<div className="content">
<div className="tip">You have unsaved changes</div>
<div className="actions">
<div className="button perdu reset">Reset</div>
<div className="button positive save-changes">Save Changes</div>
</div>
</div>
</div>
<div className="popup error">
<div className="content">
<div className="tip"></div>
<div className="actions">
<div className="button perdu close">X</div>
</div>
</div>
</div>
</div>
</div>
));
let newIconBuff: Buffer | null = null;
let oldName = guildMeta.name;
let newName = guildMeta.name;
async function updatePopups(errMsg: string | null = null) {
if (errMsg) {
q.$$$(element, '.popup.error .tip').innerText = errMsg;
q.$$$(element, '.popup.error').classList.add('enabled');
q.$$$(element, '.popup.changes').classList.remove('enabled');
await ElementsUtil.shakeElement(q.$$$(element, '.popup.error'), 400);
} else if (newIconBuff !== null || newName !== oldName) {
q.$$$(element, '.popup.changes .tip').innerText = 'You have unsaved changes'; // in case it had an error before
q.$$$(element, '.popup.changes .button.save-changes').innerText = 'Save Changes'; // in case it had try again before
q.$$$(element, '.popup.error').classList.remove('enabled');
q.$$$(element, '.popup.changes').classList.add('enabled');
} else {
q.$$$(element, '.popup.error').classList.remove('enabled');
q.$$$(element, '.popup.changes').classList.remove('enabled');
}
}
function reset() {
q.$$$<HTMLInputElement>(element, '.image-input-upload').value = '';
q.$$$<HTMLInputElement>(element, 'input.guild-name').value = oldName;
(async () => {
LOG.debug('resetting icon');
q.$$$<HTMLImageElement>(element, '.icon img').src = await ElementsUtil.getImageBufferFromResourceFailSoftly(guild, guildMeta.iconResourceId);
})();
newIconBuff = null;
newName = oldName;
}
reset(); // sets icon
q.$$$(element, '.popup.error .button.close').addEventListener('click', () => {
updatePopups()
});
q.$$$(element, '.popup.changes .button.reset').addEventListener('click', () => {
reset();
updatePopups();
});
BaseElements.bindImageUploadEvents(q.$$$(element, '.image-input-upload') as HTMLInputElement, {
maxSize: Globals.MAX_GUILD_ICON_SIZE,
acceptedMimeTypes: [ 'image/png', 'image/jpeg', 'image/jpg' ],
onChangeStart: async () => await updatePopups(),
onCleared: () => {},
onError: async (errMsg) => await updatePopups(errMsg),
onLoaded: async (buff, src) => {
LOG.debug('image loaded');
newIconBuff = buff;
(q.$$$(element, 'img.guild-icon') as HTMLImageElement).src = src;
await updatePopups();
}
});
q.$$$(element, 'input.guild-name').addEventListener('input', (e) => {
newName = q.$$$<HTMLInputElement>(element, 'input.guild-name').value;
updatePopups();
});
q.$$$(element, 'input.guild-name').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
q.$$$(element, '.button.save-changes').click();
}
});
let submitting = false;
q.$$$(element, '.button.save-changes').addEventListener('click', async () => {
if (submitting) return;
submitting = true;
q.$$$(element, '.button.save-changes').innerText = 'Saving...';
if (newName == oldName && newIconBuff == null) {
// nothing changed
updatePopups();
submitting = false;
return;
}
let success = false;
if (newName != oldName && newName.length == 0) {
LOG.warn('attempted to set empty guild name');
q.$$$(element, '.button.save-changes').innerText = 'Try Again';
q.$$$(element, '.popup.changes .tip').innerText = 'New name is empty';
await ElementsUtil.shakeElement(q.$$$(element, '.button.save-changes'), 400);
} else if (newName != oldName && newName.length > Globals.MAX_GUILD_NAME_LENGTH) {
LOG.warn('attempted to oversized guild name');
q.$$$(element, '.button.save-changes').innerText = 'Try Again';
q.$$$(element, '.popup.changes .tip').innerText = 'New name is too long. ' + newName.length + ' > ' + Globals.MAX_GUILD_NAME_LENGTH;
await ElementsUtil.shakeElement(q.$$$(element, '.button.save-changes'), 400);
} else { // client-size icon size checks are handled above
let failed = false;
// Set Name
if (newName != oldName) {
try {
await guild.requestSetGuildName(newName);
guildMeta = await guild.fetchMetadata();
} catch (e) {
LOG.error('error setting new guild name', e);
q.$$$(element, '.button.save-changes').innerText = 'Try Again';
q.$$$(element, '.popup.changes .tip').innerText = 'Error setting new guild name';
await ElementsUtil.shakeElement(q.$$$(element, '.button.save-changes'), 400);
failed = true;
}
}
// Set Icon
if (!failed && newIconBuff != null) {
try {
await guild.requestSetGuildIcon(newIconBuff);
newIconBuff = null; // prevent resubmit
} catch (e) {
LOG.error('error setting new guild icon', e);
q.$$$(element, '.button.submit').innerText = 'Try Again';
q.$$$(element, '.popup.changes .tip').innerText = 'Error setting new guild icon';
await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400);
failed = true;
}
}
success = !failed;
}
if (success) {
q.$$$(element, '.options .title').innerText = newName;
q.$$$(element, '.guild-name').setAttribute('placeholder', newName);
oldName = newName;
newIconBuff = null;
updatePopups();
}
submitting = false;
});
return element;
}

View File

@ -0,0 +1,149 @@
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import GuildsManager from '../../guilds-manager';
import { IAddGuildData } from '../overlay-add-guild';
import moment from 'moment';
import TextInput from '../components/input-text';
import ImageEditInput from '../components/input-image-edit';
import Globals from '../../globals';
import SubmitOverlayLower from '../components/submit-overlay-lower';
import path from 'path';
import fs from 'fs/promises';
import Util from '../../util';
function getExampleDisplayName(): string {
const names = [
'gamer69',
'exdenoccp',
'TiggerEliminator',
'FreshDingus',
'Wonky Experiment',
'TheLegend27'
];
return names[Math.floor(Math.random() * names.length)] as string;
}
function getExampleAvatarPath(): string {
const paths = [ // these are relative to the working directory
path.join(__dirname, '../../img/default-avatars/avatar-airstrip.png'),
path.join(__dirname, '../../img/default-avatars/avatar-building.png'),
path.join(__dirname, '../../img/default-avatars/avatar-frog.png'),
path.join(__dirname, '../../img/default-avatars/avatar-sun.png')
];
return paths[Math.floor(Math.random() * paths.length)] as string;
}
export interface AddGuildOverlayProps {
guildsManager: GuildsManager;
addGuildData: IAddGuildData;
}
const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps) => {
const { guildsManager, addGuildData } = props;
const expired = addGuildData.expires < new Date().getTime();
const exampleDisplayName = useMemo(() => getExampleDisplayName(), []);
const exampleAvatarPath = useMemo(() => getExampleAvatarPath(), []);
const [ displayName, setDisplayName ] = useState<string>(exampleDisplayName);
const [ avatarBuff, setAvatarBuff ] = useState<Buffer | null>(null);
const [ exampleAvatarFailed, setExampleAvatarFailed ] = useState<boolean>(false);
const [ addGuildFailedMessage, setAddGuildFailedMessage ] = useState<string | null>(null);
const [ displayNameInputMessage, setDisplayNameInputMessage ] = useState<string | null>(null);
const [ displayNameInputValid, setDisplayNameInputValid ] = useState<boolean>(false);
const [ avatarInputMessage, setAvatarInputMessage ] = useState<string | null>(null);
const [ avatarInputValid, setAvatarInputValid ] = useState<boolean>(false);
useEffect(() => {
(async () => {
try {
const exampleAvatarBuff = await fs.readFile(exampleAvatarPath);
if (avatarBuff === null) {
setAvatarBuff(exampleAvatarBuff);
}
} catch (e: unknown) {
LOG.error('error setting example avatar', e);
setExampleAvatarFailed(true);
}
})();
}, [])
const errorMessage = useMemo(() => {
if (exampleAvatarFailed && !avatarBuff) {
return 'Unable to load example avatar';
} else if (!avatarInputValid && avatarInputMessage) {
return avatarInputMessage;
} else if (!displayNameInputValid && displayNameInputMessage) {
return displayNameInputMessage;
} else if (addGuildFailedMessage !== null) {
return addGuildFailedMessage;
} else {
return null;
}
}, [ exampleAvatarFailed, avatarBuff, displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage, addGuildFailedMessage ]);
const doSubmit = useCallback(async (): Promise<boolean> => {
if (!displayNameInputValid || !avatarInputValid || avatarBuff === null) {
return false;
}
if (expired) {
setAddGuildFailedMessage('token expired');
return false;
}
try {
await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
setAddGuildFailedMessage(null);
return true;
} catch (e: unknown) {
LOG.error('error adding new guild', e);
if (e instanceof Error) {
setAddGuildFailedMessage(e.message);
} else {
setAddGuildFailedMessage('error adding new guild');
}
return false;
}
}, [ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]);
return (
<div className="content add-guild">
<div className="preview">
<img className="icon" src={addGuildData.iconSrc} alt="icon"></img>
<div>
<div className="name">{addGuildData.name}</div>
<div className="url">{addGuildData.url}</div>
<div className="expires">{(expired ? 'Invite Expired ' : 'Invite Expires ') + moment(addGuildData.expires).fromNow()}</div>
</div>
</div>
<div className="divider"></div>
<div className="personalization">
<div className="avatar">
<ImageEditInput
maxSize={Globals.MAX_AVATAR_SIZE}
value={avatarBuff} setValue={setAvatarBuff}
setValid={setAvatarInputValid} setMessage={setAvatarInputMessage}
/>
</div>
<div className="display-name">
<TextInput
label="Display Name" placeholder={exampleDisplayName}
maxLength={Globals.MAX_DISPLAY_NAME_LENGTH}
value={displayName} setValue={setDisplayName}
setValid={setDisplayNameInputValid} setMessage={setDisplayNameInputMessage}
/>
</div>
</div>
<SubmitOverlayLower buttonMessage="Add Guild" doSubmit={doSubmit} errorMessage={errorMessage} />
</div>
);
}
export default AddGuildOverlay;

View File

@ -113,4 +113,38 @@
}
}
}
}
.lower-react {
.error-container {
background-color: $background-secondary-alt;
overflow-y: hidden;
max-height: 0;
transition: max-height 0.2s ease-in-out;
&:not(.empty) {
max-height: 48px;
}
.error {
font-size: 16px;
line-height: 1;
padding: 16px 16px;
height: 16px;
color: $text-normal;
&::first-letter {
text-transform: uppercase;
}
}
}
.buttons {
padding: 16px;
display: flex;
justify-content: flex-end;
background-color: $background-tertiary;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
}

View File

@ -234,7 +234,7 @@ body > .overlay,
width: 64px;
height: 64px;
margin-right: 16px;
border-radius: 16px;
border-radius: 8px;
}
.preview .name {
@ -253,52 +253,34 @@ body > .overlay,
font-weight: 600;
}
.message {
margin: 16px;
padding: 0;
}
.avatar-input {
.personalization {
margin: 16px;
display: flex;
align-items: center;
}
.display-name-input {
margin: 16px;
color: $text-normal;
background-color: $channeltextarea-background;
border-radius: 8px;
max-height: 100px;
overflow-y: scroll;
padding: 14px 16px;
}
.display-name-input:focus {
outline: none;
.display-name {
margin-left: 16px;
}
}
.lower {
padding: 16px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: $background-tertiary;
}
.error {
color: $text-normal;
}
.error::first-letter {
text-transform: uppercase;
}
.buttons {
margin-left: 16px;
display: flex;
.error {
background-color: $background-secondary-alt;
color: $text-normal;
}
.error::first-letter {
text-transform: uppercase;
}
.buttons {
margin-left: 16px;
display: flex;
}
}
}