reactify add guild overlay
This commit is contained in:
parent
392a8cf4bc
commit
f57ef4848f
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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>
|
||||
|
@ -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);
|
@ -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;
|
||||
}
|
149
src/client/webapp/elements/overlays/overlay-add-guild.tsx
Normal file
149
src/client/webapp/elements/overlays/overlay-add-guild.tsx
Normal 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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user