@fortawesome/free-solid-svg-icons#faTrash TypeScript Examples
The following examples show how to use
@fortawesome/free-solid-svg-icons#faTrash.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: ComponentsPanel.tsx From MagicUI with Apache License 2.0 | 6 votes |
function DeletePageButton(props: { onClick: () => void }) {
return (
<div className={style.delete_page_button} onClick={props.onClick}>
<button>
<FontAwesomeIcon icon={faTrash}/>
</button>
</div>
);
}
Example #2
Source File: shared.module.ts From enterprise-ng-2020-workshop with MIT License | 6 votes |
constructor(faIconLibrary: FaIconLibrary) {
faIconLibrary.addIcons(
faGithub,
faMediumM,
faPlus,
faEdit,
faTrash,
faTimes,
faCaretUp,
faCaretDown,
faExclamationTriangle,
faFilter,
faTasks,
faCheck,
faSquare,
faLanguage,
faPaintBrush,
faLightbulb,
faWindowMaximize,
faStream,
faBook
);
}
Example #3
Source File: fontawesome-config.ts From covidnet_ui with GNU Affero General Public License v3.0 | 6 votes |
// Description: add icons to be used in the app as needed
// Some are solid and some are from regular svg (hollow icons)
library.add(
faSearch,
faSearchPlus,
faHome,
faSpinner,
faExclamationTriangle,
faTrash,
faTrashAlt,
faEdit,
faUser,
faUserEdit,
faUserPlus,
faUserTimes,
faUserMinus,
faKey,
faCheck,
faCheckCircle,
faCalendarDay,
faClock,
faCalendarAlt,
farUser,
faFileAlt
);
Example #4
Source File: package-detailed.component.ts From msfs-community-downloader with GNU Affero General Public License v3.0 | 5 votes |
faTrash = faTrash;
Example #5
Source File: EditTools.tsx From MagicUI with Apache License 2.0 | 5 votes |
export default function EditTools(props: IEditToolsProps) {
const cpnState = useSelector((state: IStoreState) => state.component);
const editToolsState = useSelector((state: IStoreState) => state.editTools);
const webGLPage = useSelector((state: IStoreState) => state.webGLPage);
const dispatch = useDispatch();
const undo = () => {
dispatch(undoComponent(cpnState.id));
toast('undo!');
};
const del = () => {
dispatch(deleteComponent(cpnState.id));
toast('delete!');
};
const paste = () => {
dispatch(pasteComponent(editToolsState.id))
toast('paste!');
};
const copy = () => {
dispatch(copyComponent(cpnState.id));
toast('copy!');
};
const cut = () => {
dispatch(copyComponent(cpnState.id));
dispatch(deleteComponent(cpnState.id));
toast('cut!');
};
const save = () => {
dispatch(saveComponent())
};
return (
<div className={style.edit_tools}>
<div className={style.label}>
EDIT:
</div>
<div className={style.tools}>
<button className={style.save_btn}>
<FontAwesomeIcon icon={faSave} onClick={save}/>
</button>
<button className={style.undo_btn} onClick={undo}>
<FontAwesomeIcon icon={faUndo}/>
</button>
<button className={style.cut_btn} onClick={cut}>
<FontAwesomeIcon icon={faCut}/>
</button>
<button className={style.copy_btn} onClick={copy}>
<FontAwesomeIcon icon={faCopy}/>
</button>
<button className={style.paste_btn} onClick={paste}>
<FontAwesomeIcon icon={faPaste}/>
</button>
<button className={style.trash_btn} onClick={del}>
<FontAwesomeIcon icon={faTrash}/>
</button>
</div>
</div>
);
}
Example #6
Source File: FileSystem.tsx From MagicUI with Apache License 2.0 | 5 votes |
function ProjectManageTools() {
const user = useSelector((state: IStoreState) => state.user);
const dslFile = useSelector((state: IStoreState) => state.dslFile);
const openFileItems = useSelector((state: IStoreState) => state.openFileItems);
const dispatch = useDispatch();
const handleCreateFile = () => {
modal(cancel => (
<NewFileOrFolderModal fileType="file" cancel={cancel} email={user.email} folder={dslFile.folder}
dispatch={dispatch}/>
));
};
const handleCreateFolder = () => {
modal(cancel => (
<NewFileOrFolderModal fileType="folder" cancel={cancel} email={user.email} folder={dslFile.folder}
dispatch={dispatch}/>
));
};
const handleDeleteFile = () => {
modal(cancel => <Confirm title={`Do you want to delete ${dslFile.filename}`} cancel={cancel} confirm={() => {
deleteDslFile(user.email, dslFile.id, dslFile.fileId).then(v => {
if (!v.err) {
toast('delete file!');
dispatch(localDeleteFile(dslFile.id));
let i = 0, check = false;
for (let item of openFileItems.items) {
if (v.id === item.id) {
check = true;
break
}
i++;
}
console.log(check, i - 1);
check && dispatch(closeFile(i - 1, dslFile.id, dslFile.fileId));
cancel();
}
});
}}/>);
};
return (
<div className={style.project_manage_tools}>
<button className={style.mk_file_btn} onClick={handleCreateFile}>
<FontAwesomeIcon icon={faPlus}/>
</button>
<button className={style.mk_dir_btn} onClick={handleCreateFolder}>
<FontAwesomeIcon icon={faFolderPlus}/>
</button>
<button className={style.delete_btn} onClick={handleDeleteFile}>
<FontAwesomeIcon icon={faTrash}/>
</button>
</div>
);
}
Example #7
Source File: icons.font-awesome-solid.ts From dayz-server-manager with MIT License | 5 votes |
fontAwesomeSolidIcons = { faAngleDown, faAngleRight, faArrowLeft, faBars, faBookOpen, faChartArea, faChartBar, faChartPie, faChevronDown, faChevronUp, faColumns, faSearch, faTable, faTachometerAlt, faUser, faExclamationTriangle, faSignOutAlt, faCalendarAlt, faCogs, faClipboardList, faHammer, faTools, faSync, faLock, faLockOpen, faTrash, faPlusCircle, faSpinner, faMap, faAnchor, faCity, faChessRook, faMountain, faCampground, faHome, faUniversity, faCrosshairs, faPlane, faWrench, }
Example #8
Source File: employee-list.component.ts From employee-crud-api with MIT License | 5 votes |
icons = { faTrash, faEdit }
Example #9
Source File: icon.service.ts From WowUp with GNU General Public License v3.0 | 5 votes |
public constructor(private _matIconRegistry: MatIconRegistry, private _sanitizer: DomSanitizer) {
this.addSvg(faAngleDoubleDown);
this.addSvg(faArrowUp);
this.addSvg(faArrowDown);
this.addSvg(faSyncAlt);
this.addSvg(faTimes);
this.addSvg(faExternalLinkAlt);
this.addSvg(faQuestionCircle);
this.addSvg(faPlay);
this.addSvg(faClock);
this.addSvg(faBug);
this.addSvg(faLink);
this.addSvg(faDiscord);
this.addSvg(faGithub);
this.addSvg(faInfoCircle);
this.addSvg(faCodeBranch);
this.addSvg(faCaretDown);
this.addSvg(faExclamationTriangle);
this.addSvg(faCode);
this.addSvg(faPatreon);
this.addSvg(faCoins);
this.addSvg(faCompressArrowsAlt);
this.addSvg(faPencilAlt);
this.addSvg(faCheckCircle);
this.addSvg(faDiceD6);
this.addSvg(faSearch);
this.addSvg(faInfoCircle);
this.addSvg(faNewspaper);
this.addSvg(faCog);
this.addSvg(faAngleUp);
this.addSvg(faAngleDown);
this.addSvg(faChevronRight);
this.addSvg(faUserCircle);
this.addSvg(faEllipsisV);
this.addSvg(faCopy);
this.addSvg(farCheckCircle);
this.addSvg(faExclamation);
this.addSvg(faTrash);
this.addSvg(faHistory);
this.addSvg(faCaretSquareRight);
this.addSvg(faCaretSquareLeft);
this.addSvg(faMinimize);
this.addSvg(faUpRightFromSquare);
}
Example #10
Source File: ProbabilityInput.tsx From calculate-my-odds with MIT License | 5 votes |
render() {
return (
<div className="probability-input-component">
<div className="probability-name">
<Input
type="text"
placeholder="Name"
value={this.props.item.name}
onChange={e => this.props.onChange({
...this.props.item,
name: e.target.value
})}
/>
</div>
<div className="probability-chance">
<TooltipContainer
tooltipContent={this.getTooltipErrorContent()}
showOnHover={!this.state.isProbabilityInputFocused && this.shouldMarkError()}
show={this.state.showInvalidProbabilityError}
side={TooltipSide.Right}
maxWidth={"11em"}
>
<Input
type="text"
placeholder="Probability"
value={this.props.item.probabilityDisplay}
onChange={e => {
this.props.onChange({
...this.props.item,
probability: parseProbability(e.target.value),
probabilityDisplay: e.target.value
});
this.setState({
showInvalidProbabilityError: false
});
}}
markError={this.shouldMarkError()}
onlyMarkErrorOnBlur
onFocus={() => this.setState({
isProbabilityInputFocused: true,
showInvalidProbabilityError: false
})}
onBlur={() => this.setState({
isProbabilityInputFocused: false
})}
/>
</TooltipContainer>
</div>
{this.props.showDeleteButton &&
<div className="probability-remove">
<IconContainer
icon={faTrash}
onClick={this.props.onDeleteRequest}
/>
</div>
}
</div>
);
}
Example #11
Source File: SingularCompletionGoalInput.tsx From calculate-my-odds with MIT License | 5 votes |
render() {
return (
<div className="probability-goal-input-component">
<div className="goal-probability-item">
<TooltipContainer
tooltipContent="An option must be selected."
show={this.state.showNoItemSelectedError}
side={TooltipSide.Left}
>
<Select
options={this.props.itemNames.map(x => ({ name: x }))}
value={this.props.goal.itemName ? { name: this.props.goal.itemName } : undefined}
onChange={(x) => {
this.props.onChange({
...this.props.goal,
itemName: x?.name,
});
this.setState({
showNoItemSelectedError: false
});
}}
getOptionLabel={x => x.name}
getOptionValue={x => x.name}
markError={this.state.showNoItemSelectedError}
/>
</TooltipContainer>
</div>
<div className="goal-comparison-operator">
<Select
options={comparisonOperators}
getOptionLabel={(x) => x.type}
getOptionValue={(x) => x.type}
value={this.props.goal.comparator}
onChange={(x) =>
this.props.onChange({
...this.props.goal,
comparator: x!,
})
}
/>
</div>
<div className="goal-target-container">
<TooltipContainer
tooltipContent="A quantity must be entered."
show={this.state.showNoTargetValueError}
side={TooltipSide.Left}
>
<IntegerInput
placeholder="Quantity"
value={this.props.goal.targetCount}
onChange={(x) => {
this.props.onChange({
...this.props.goal,
targetCount: x,
});
this.setState({
showNoTargetValueError: false
});
}}
onFocus={() => this.setState({
showNoTargetValueError: false
})}
markError={this.state.showNoTargetValueError}
/>
</TooltipContainer>
</div>
<div className="goal-button-container">
<IconContainer
icon={faTrash}
onClick={this.props.onDeleteRequest}
/>
</div>
</div>
);
}
Example #12
Source File: SingularCompletionFailureInput.tsx From calculate-my-odds with MIT License | 5 votes |
render() {
return (
<div className="probability-failure-input-component">
<div className="failure-probability-item">
<TooltipContainer
tooltipContent="An option must be selected."
show={this.state.showNoItemSelectedError}
side={TooltipSide.Left}
>
<Select
options={this.props.itemNames.map(x => ({ name: x }))}
value={this.props.failure.itemName ? { name: this.props.failure.itemName } : undefined}
onChange={(x) => {
this.props.onChange({
...this.props.failure,
itemName: x?.name,
});
this.setState({
showNoItemSelectedError: false
});
}}
getOptionLabel={x => x.name}
getOptionValue={x => x.name}
markError={this.state.showNoItemSelectedError}
/>
</TooltipContainer>
</div>
<div className="failure-comparison-operator">
<Select
options={comparisonOperators}
getOptionLabel={(x) => x.type}
getOptionValue={(x) => x.type}
value={this.props.failure.comparator}
onChange={(x) =>
this.props.onChange({
...this.props.failure,
comparator: x!,
})
}
/>
</div>
<div className="failure-target-container">
<TooltipContainer
tooltipContent="A quantity must be entered."
show={this.state.showNoTargetValueError}
side={TooltipSide.Left}
>
<IntegerInput
placeholder="Quantity"
value={this.props.failure.targetCount}
onChange={(x) => {
this.props.onChange({
...this.props.failure,
targetCount: x,
});
this.setState({
showNoTargetValueError: false
});
}}
onFocus={() => this.setState({
showNoTargetValueError: false
})}
markError={this.state.showNoTargetValueError}
/>
</TooltipContainer>
</div>
<div className="failure-button-container">
<IconContainer
icon={faTrash}
onClick={this.props.onDeleteRequest}
/>
</div>
</div>
);
}
Example #13
Source File: Editor.tsx From zwo-editor with MIT License | 4 votes |
Editor = ({ match }: RouteComponentProps<TParams>) => {
const { v4: uuidv4 } = require("uuid");
const S3_URL = "https://zwift-workout.s3-eu-west-1.amazonaws.com";
const [id, setId] = useState(
match.params.id === "new"
? localStorage.getItem("id") || generateId()
: match.params.id
);
const [bars, setBars] = useState<Array<Bar>>(
JSON.parse(localStorage.getItem("currentWorkout") || "[]")
);
const [actionId, setActionId] = useState<string | undefined>(undefined);
const [ftp, setFtp] = useState(
parseInt(localStorage.getItem("ftp") || "200")
);
const [weight, setWeight] = useState(
parseInt(localStorage.getItem("weight") || "75")
);
const [instructions, setInstructions] = useState<Array<Instruction>>(
JSON.parse(localStorage.getItem("instructions") || "[]")
);
const [tags, setTags] = useState(
JSON.parse(localStorage.getItem("tags") || "[]")
);
const [name, setName] = useState(localStorage.getItem("name") || "");
const [description, setDescription] = useState(
localStorage.getItem("description") || ""
);
const [author, setAuthor] = useState(localStorage.getItem("author") || "");
const [savePopupIsVisile, setSavePopupVisibility] = useState(false);
const [sharePopupIsVisile, setSharePopupVisibility] = useState(false);
const [user, setUser] = useState<firebase.User | null>(null);
const [visibleForm, setVisibleForm] = useState("login"); // default form is login
const canvasRef = useRef<HTMLInputElement>(null);
const segmentsRef = useRef<HTMLInputElement>(null);
const [segmentsWidth, setSegmentsWidth] = useState(1320);
const [message, setMessage] = useState<Message>();
const [showWorkouts, setShowWorkouts] = useState(false);
// bike or run
const [sportType, setSportType] = useState<SportType>(
(localStorage.getItem("sportType") as SportType) || "bike"
);
// distance or time
const [durationType, setDurationType] = useState<DurationType>(
(localStorage.getItem("durationType") as DurationType) || "time"
);
const [runningTimes, setRunningTimes] = useState(loadRunningTimes());
const [textEditorIsVisible, setTextEditorIsVisible] = useState(false);
const [selectedInstruction, setSelectedInstruction] = useState<Instruction>();
const db = firebase.database();
const location = useLocation();
useEffect(() => {
const params = new URLSearchParams(location.search);
const mode = params.get("mode");
if (mode === "resetPassword") {
setVisibleForm("updatePassword");
setSavePopupVisibility(true);
}
}, [location]);
useEffect(() => {
setMessage({ visible: true, class: "loading", text: "Loading.." });
db.ref("workouts/" + id)
.once("value")
.then(function (snapshot) {
if (snapshot.val()) {
// workout exist on server
setAuthor(snapshot.val().author);
setName(snapshot.val().name);
setDescription(snapshot.val().description);
setBars(snapshot.val().workout || []);
setInstructions(snapshot.val().instructions || []);
setTags(snapshot.val().tags || []);
setDurationType(snapshot.val().durationType);
setSportType(snapshot.val().sportType);
localStorage.setItem("id", id);
} else {
// workout doesn't exist on cloud
if (id === localStorage.getItem("id")) {
// user refreshed the page
} else {
// treat this as new workout
setBars([]);
setInstructions([]);
setName("");
setDescription("");
setAuthor("");
setTags([]);
}
localStorage.setItem("id", id);
}
console.log("useEffect firebase");
//finished loading
setMessage({ visible: false });
});
auth.onAuthStateChanged((user) => {
if (user) {
setUser(user);
}
});
window.history.replaceState("", "", `/editor/${id}`);
ReactGA.initialize("UA-55073449-9");
ReactGA.pageview(window.location.pathname + window.location.search);
}, [id, db]);
useEffect(() => {
localStorage.setItem("currentWorkout", JSON.stringify(bars));
localStorage.setItem("ftp", ftp.toString());
localStorage.setItem("instructions", JSON.stringify(instructions));
localStorage.setItem("weight", weight.toString());
localStorage.setItem("name", name);
localStorage.setItem("description", description);
localStorage.setItem("author", author);
localStorage.setItem("tags", JSON.stringify(tags));
localStorage.setItem("sportType", sportType);
localStorage.setItem("durationType", durationType);
localStorage.setItem("runningTimes", JSON.stringify(runningTimes));
setSegmentsWidth(segmentsRef.current?.scrollWidth || 1320);
}, [
segmentsRef,
bars,
ftp,
instructions,
weight,
name,
description,
author,
tags,
sportType,
durationType,
runningTimes,
]);
function generateId() {
return Math.random().toString(36).substr(2, 16);
}
function newWorkout() {
console.log("New workout");
setId(generateId());
setBars([]);
setInstructions([]);
setName("");
setDescription("");
setAuthor("");
setTags([]);
}
function handleOnChange(id: string, values: Bar) {
const index = bars.findIndex((bar) => bar.id === id);
const updatedArray = [...bars];
updatedArray[index] = values;
setBars(updatedArray);
}
function handleOnClick(id: string) {
if (id === actionId) {
setActionId(undefined);
} else {
setActionId(id);
}
}
function handleKeyPress(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.target instanceof HTMLInputElement) {
// Ignore key presses coming from input elements
return;
}
if (event.target instanceof HTMLTextAreaElement) {
// Ignore key presses coming from textarea elements
return;
}
switch (event.keyCode) {
case 8:
removeBar(actionId || "");
// Prevent navigation to previous page
event.preventDefault();
break;
case 37:
// reduce time
removeTimeToBar(actionId || "");
break;
case 39:
// add time
addTimeToBar(actionId || "");
break;
case 38:
// add power
addPowerToBar(actionId || "");
break;
case 40:
// add power
removePowerToBar(actionId || "");
break;
default:
//console.log(event.keyCode);
break;
}
}
function addBar(
zone: number,
duration: number = 300,
cadence: number = 0,
pace: number = 0,
length: number = 200
) {
setBars((bars) => [
...bars,
{
time:
durationType === "time"
? duration
: helpers.round(
helpers.calculateTime(length, calculateSpeed(pace)),
1
),
length:
durationType === "time"
? helpers.round(
helpers.calculateDistance(duration, calculateSpeed(pace)),
1
)
: length,
power: zone,
cadence: cadence,
type: "bar",
id: uuidv4(),
pace: pace,
},
]);
}
function addTrapeze(
zone1: number,
zone2: number,
duration: number = 300,
pace: number = 0,
length: number = 1000,
cadence: number = 0
) {
setBars((bars) => [
...bars,
{
time:
durationType === "time"
? duration
: helpers.round(
helpers.calculateTime(length, calculateSpeed(pace)),
1
),
length:
durationType === "time"
? helpers.round(
helpers.calculateDistance(duration, calculateSpeed(pace)),
1
)
: length,
startPower: zone1,
endPower: zone2,
cadence: cadence,
pace: pace,
type: "trapeze",
id: uuidv4(),
},
]);
}
function addFreeRide(
duration: number = 600,
cadence: number = 0,
length: number = 1000
) {
setBars((bars) => [
...bars,
{
time: durationType === "time" ? duration : 0,
length: durationType === "time" ? 0 : length,
cadence: cadence,
type: "freeRide",
id: uuidv4(),
},
]);
}
function addInterval(
repeat: number = 3,
onDuration: number = 30,
offDuration: number = 120,
onPower: number = 1,
offPower: number = 0.5,
cadence: number = 0,
restingCadence: number = 0,
pace: number = 0,
onLength: number = 200,
offLength: number = 200
) {
setBars((bars) => [
...bars,
{
time:
durationType === "time"
? (onDuration + offDuration) * repeat
: helpers.round(
helpers.calculateTime(
(onLength + offLength) * repeat,
calculateSpeed(pace)
),
1
),
length:
durationType === "time"
? helpers.round(
helpers.calculateDistance(
(onDuration + offDuration) * repeat,
calculateSpeed(pace)
),
1
)
: (onLength + offLength) * repeat,
id: uuidv4(),
type: "interval",
cadence: cadence,
restingCadence: restingCadence,
repeat: repeat,
onDuration:
durationType === "time"
? onDuration
: helpers.round(
helpers.calculateTime(
(onLength * 1) / onPower,
calculateSpeed(pace)
),
1
),
offDuration:
durationType === "time"
? offDuration
: helpers.round(
helpers.calculateTime(
(offLength * 1) / offPower,
calculateSpeed(pace)
),
1
),
onPower: onPower,
offPower: offPower,
pace: pace,
onLength:
durationType === "time"
? helpers.round(
helpers.calculateDistance(
(onDuration * 1) / onPower,
calculateSpeed(pace)
),
1
)
: onLength,
offLength:
durationType === "time"
? helpers.round(
helpers.calculateDistance(
(offDuration * 1) / offPower,
calculateSpeed(pace)
),
1
)
: offLength,
},
]);
}
function addInstruction(text = "", time = 0, length = 0) {
setInstructions((instructions) => [
...instructions,
{
text: text,
time: time,
length: length,
id: uuidv4(),
},
]);
}
function changeInstruction(id: string, values: Instruction) {
const index = instructions.findIndex(
(instructions) => instructions.id === id
);
const updatedArray = [...instructions];
updatedArray[index] = values;
setInstructions(updatedArray);
}
function deleteInstruction(id: string) {
const updatedArray = [...instructions];
setInstructions(updatedArray.filter((item) => item.id !== id));
}
function removeBar(id: string) {
const updatedArray = [...bars];
setBars(updatedArray.filter((item) => item.id !== id));
setActionId(undefined);
}
function addTimeToBar(id: string) {
const updatedArray = [...bars];
const index = updatedArray.findIndex((bar) => bar.id === id);
const element = updatedArray[index];
if (element && durationType === "time") {
element.time = element.time + 5;
element.length =
(helpers.calculateDistance(
element.time,
calculateSpeed(element.pace || 0)
) *
1) /
(element.power || 1);
setBars(updatedArray);
}
if (element && durationType === "distance") {
element.length = (element.length || 0) + 200;
element.time =
(helpers.calculateTime(
element.length,
calculateSpeed(element.pace || 0)
) *
1) /
(element.power || 1);
setBars(updatedArray);
}
}
function removeTimeToBar(id: string) {
const updatedArray = [...bars];
const index = updatedArray.findIndex((bar) => bar.id === id);
const element = updatedArray[index];
if (element && element.time > 5 && durationType === "time") {
element.time = element.time - 5;
element.length =
(helpers.calculateDistance(
element.time,
calculateSpeed(element.pace || 0)
) *
1) /
(element.power || 1);
setBars(updatedArray);
}
if (element && (element.length || 0) > 200 && durationType === "distance") {
element.length = (element.length || 0) - 200;
element.time =
(helpers.calculateTime(
element.length,
calculateSpeed(element.pace || 0)
) *
1) /
(element.power || 1);
setBars(updatedArray);
}
}
function addPowerToBar(id: string) {
const updatedArray = [...bars];
const index = updatedArray.findIndex((bar) => bar.id === id);
const element = updatedArray[index];
if (element && element.power) {
element.power = parseFloat((element.power + 1 / ftp).toFixed(3));
if (durationType === "time") {
element.length =
(helpers.calculateDistance(
element.time,
calculateSpeed(element.pace || 0)
) *
1) /
element.power;
} else {
element.time =
(helpers.calculateTime(
element.length,
calculateSpeed(element.pace || 0)
) *
1) /
element.power;
}
setBars(updatedArray);
}
}
function removePowerToBar(id: string) {
const updatedArray = [...bars];
const index = updatedArray.findIndex((bar) => bar.id === id);
const element = updatedArray[index];
if (element && element.power && element.power >= Zones.Z1.min) {
element.power = parseFloat((element.power - 1 / ftp).toFixed(3));
if (durationType === "time") {
element.length =
(helpers.calculateDistance(
element.time,
calculateSpeed(element.pace || 0)
) *
1) /
element.power;
} else {
element.time =
(helpers.calculateTime(
element.length,
calculateSpeed(element.pace || 0)
) *
1) /
element.power;
}
setBars(updatedArray);
}
}
function duplicateBar(id: string) {
const index = bars.findIndex((bar) => bar.id === id);
const element = [...bars][index];
if (element.type === "bar")
addBar(
element.power || 80,
element.time,
element.cadence,
element.pace,
element.length
);
if (element.type === "freeRide")
addFreeRide(element.time, element.cadence, element.length);
if (element.type === "trapeze")
addTrapeze(
element.startPower || 80,
element.endPower || 160,
element.time,
element.pace || 0,
element.length,
element.cadence
);
if (element.type === "interval")
addInterval(
element.repeat,
element.onDuration,
element.offDuration,
element.onPower,
element.offPower,
element.cadence,
element.restingCadence,
element.pace,
element.onLength,
element.offLength
);
setActionId(undefined);
}
function moveLeft(id: string) {
const index = bars.findIndex((bar) => bar.id === id);
// not first position of array
if (index > 0) {
const updatedArray = [...bars];
const element = [...bars][index];
updatedArray.splice(index, 1);
updatedArray.splice(index - 1, 0, element);
setBars(updatedArray);
}
}
function moveRight(id: string) {
const index = bars.findIndex((bar) => bar.id === id);
// not first position of array
if (index < bars.length - 1) {
const updatedArray = [...bars];
const element = [...bars][index];
updatedArray.splice(index, 1);
updatedArray.splice(index + 1, 0, element);
setBars(updatedArray);
}
}
function saveWorkout() {
setSavePopupVisibility(true);
}
function deleteWorkout() {
// save to cloud (firebase) if logged in
if (user) {
const itemsRef = firebase.database().ref();
var updates: any = {};
updates[`users/${user.uid}/workouts/${id}`] = null;
updates[`workouts/${id}`] = null;
// save to firebase
itemsRef
.update(updates)
.then(() => {
newWorkout();
})
.catch((error) => {
console.log(error);
setMessage({
visible: true,
class: "error",
text: "Cannot delete workout",
});
});
}
}
function shareWorkout() {
if (user) {
save();
setSharePopupVisibility(true);
} else {
saveWorkout();
}
}
function save() {
setMessage({ visible: true, class: "loading", text: "Saving.." });
const xml = createWorkoutXml({
author,
name,
description,
sportType,
durationType,
tags,
bars,
instructions,
});
const file = new Blob([xml], { type: "application/xml" });
// save to cloud (firebase) if logged in
if (user) {
const itemsRef = firebase.database().ref();
const item = {
id: id,
name: name,
description: description,
author: author,
workout: bars,
tags: tags,
instructions: instructions,
userId: user.uid,
updatedAt: Date(),
sportType: sportType,
durationType: durationType,
};
const item2 = {
name: name,
description: description,
updatedAt: Date(),
sportType: sportType,
durationType: durationType,
workoutTime: helpers.formatDuration(
helpers.getWorkoutLength(bars, durationType)
),
workoutDistance: helpers.getWorkoutDistance(bars),
};
var updates: any = {};
updates[`users/${user.uid}/workouts/${id}`] = item2;
updates[`workouts/${id}`] = item;
// save to firebase
itemsRef
.update(updates)
.then(() => {
//upload to s3
upload(file, false);
setMessage({ visible: false });
})
.catch((error) => {
console.log(error);
setMessage({
visible: true,
class: "error",
text: "Cannot save this",
});
});
} else {
// download workout without saving
setMessage({ visible: false });
}
return file;
}
function logout() {
console.log("logout");
auth.signOut().then(() => setUser(null));
}
function downloadWorkout() {
const tempFile = save();
const url = window.URL.createObjectURL(tempFile);
var a = document.createElement("a");
document.body.appendChild(a);
a.style.display = "none";
a.href = url;
a.download = `${id}.zwo`;
a.click();
window.URL.revokeObjectURL(url);
}
function handleUpload(file: Blob) {
// ask user if they want to overwrite current workout first
if (bars.length > 0) {
if (!window.confirm("Are you sure you want to create a new workout?")) {
return false;
}
}
newWorkout();
upload(file, true);
}
function upload(file: Blob, parse = false) {
fetch(process.env.REACT_APP_UPLOAD_FUNCTION || "https://zwiftworkout.netlify.app/.netlify/functions/upload", {
method: "POST",
body: JSON.stringify({
fileType: "zwo",
fileName: `${id}.zwo`,
}),
})
.then((res) => res.json())
.then(function (data) {
const signedUrl = data.uploadURL;
// upload to S3
fetch(signedUrl, {
method: "PUT",
headers: {
"Content-Type": "zwo",
},
body: file,
})
.then((response) => response.text())
.then((data) => {
console.log("File uploaded");
// can parse now
if (parse) fetchAndParse(id);
})
.catch((error) => {
console.error(error);
});
});
}
function fetchAndParse(id: string) {
// remove previous workout
// TODO fix for running distance based
setBars([]);
setInstructions([]);
fetch(`${S3_URL}/${id}.zwo`)
.then((response) => response.text())
.then((data) => {
// remove xml comments
data = data.replace(/<!--(.*?)-->/gm, "");
//now parse file
const workout = Converter.xml2js(data);
const workout_file = workout.elements[0];
if (workout_file.name === "workout_file") {
// file is valid
const authorIndex = workout_file.elements.findIndex(
(element: { name: string }) => element.name === "author"
);
if (
authorIndex !== -1 &&
workout_file.elements[authorIndex].elements
) {
setAuthor(workout_file.elements[authorIndex].elements[0].text);
}
const nameIndex = workout_file.elements.findIndex(
(element: { name: string }) => element.name === "name"
);
if (nameIndex !== -1 && workout_file.elements[nameIndex].elements) {
setName(workout_file.elements[nameIndex].elements[0].text);
}
const descriptionIndex = workout_file.elements.findIndex(
(element: { name: string }) => element.name === "description"
);
if (
descriptionIndex !== -1 &&
workout_file.elements[descriptionIndex].elements
) {
setDescription(
workout_file.elements[descriptionIndex].elements[0].text
);
}
const workoutIndex = workout_file.elements.findIndex(
(element: { name: string }) => element.name === "workout"
);
var totalTime = 0;
workout_file.elements[workoutIndex].elements.map(
(w: {
name: string;
attributes: {
Power: any;
PowerLow: string;
Duration: string;
PowerHigh: string;
Cadence: string;
CadenceResting: string;
Repeat: string;
OnDuration: string;
OffDuration: string;
OnPower: string;
OffPower: string;
Pace: string;
};
elements: any;
}) => {
let duration = parseFloat(w.attributes.Duration);
if (w.name === "SteadyState")
addBar(
parseFloat(w.attributes.Power || w.attributes.PowerLow),
parseFloat(w.attributes.Duration),
parseFloat(w.attributes.Cadence || "0"),
parseInt(w.attributes.Pace || "0")
);
if (
w.name === "Ramp" ||
w.name === "Warmup" ||
w.name === "Cooldown"
)
addTrapeze(
parseFloat(w.attributes.PowerLow),
parseFloat(w.attributes.PowerHigh),
parseFloat(w.attributes.Duration),
parseInt(w.attributes.Pace || "0"),
undefined,
parseInt(w.attributes.Cadence)
);
if (w.name === "IntervalsT") {
addInterval(
parseFloat(w.attributes.Repeat),
parseFloat(w.attributes.OnDuration),
parseFloat(w.attributes.OffDuration),
parseFloat(w.attributes.OnPower),
parseFloat(w.attributes.OffPower),
parseInt(w.attributes.Cadence || "0"),
parseInt(w.attributes.CadenceResting),
parseInt(w.attributes.Pace || "0")
);
duration =
(parseFloat(w.attributes.OnDuration) +
parseFloat(w.attributes.OffDuration)) *
parseFloat(w.attributes.Repeat);
}
if (w.name === "FreeRide")
addFreeRide(
parseFloat(w.attributes.Duration),
parseInt(w.attributes.Cadence)
);
// check for instructions
const textElements = w.elements;
if (textElements && textElements.length > 0) {
textElements.map(
(t: {
name: string;
attributes: {
message: string | undefined;
timeoffset: string;
};
}) => {
if (t.name.toLowerCase() === "textevent")
addInstruction(
t.attributes.message,
totalTime + parseFloat(t.attributes.timeoffset)
);
return false;
}
);
}
totalTime = totalTime + duration;
// map functions expect return value
return false;
}
);
}
})
.catch((error) => {
console.error(error);
});
}
function calculateSpeed(pace: number = 0) {
if (sportType === "bike") {
return 0;
} else {
// return speed in m/s
// speed = distance / time
const distances = [1.60934, 5, 10, 21.0975, 42.195];
const times = [
runningTimes.oneMile,
runningTimes.fiveKm,
runningTimes.tenKm,
runningTimes.halfMarathon,
runningTimes.marathon,
];
return (distances[pace] * 1000) / helpers.getTimeinSeconds(times[pace]);
}
}
const renderBar = (bar: Bar) => (
<Bar
key={bar.id}
id={bar.id}
time={bar.time}
length={bar.length || 200}
power={bar.power || 100}
cadence={bar.cadence}
ftp={ftp}
weight={weight}
sportType={sportType}
durationType={durationType}
pace={bar.pace || 0}
speed={calculateSpeed(bar.pace || 0)}
onChange={(id: string, value: any) => handleOnChange(id, value)} // Change any to Interface Bar?
onClick={(id: string) => handleOnClick(id)}
selected={bar.id === actionId}
showLabel={true}
/>
);
const renderTrapeze = (bar: Bar) => (
<Trapeze
key={bar.id}
id={bar.id}
time={bar.time}
length={bar.length || 200}
cadence={bar.cadence}
startPower={bar.startPower || 80}
endPower={bar.endPower || 160}
ftp={ftp}
sportType={sportType}
durationType={durationType}
pace={bar.pace || 0}
speed={calculateSpeed(bar.pace || 0)}
onChange={(id: string, value: any) => handleOnChange(id, value)} // Change any to Interface Bar?
onClick={(id: string) => handleOnClick(id)}
selected={bar.id === actionId}
/>
);
const renderFreeRide = (bar: Bar) => (
<FreeRide
key={bar.id}
id={bar.id}
time={bar.time}
length={bar.length}
cadence={bar.cadence}
durationType={durationType}
sportType={sportType}
onChange={(id: string, value: any) => handleOnChange(id, value)} // Change any to Interface Bar?
onClick={(id: string) => handleOnClick(id)}
selected={bar.id === actionId}
/>
);
const renderInterval = (bar: Bar) => (
<Interval
key={bar.id}
id={bar.id}
repeat={bar.repeat || 3}
onDuration={bar.onDuration || 10}
offDuration={bar.offDuration || 50}
onPower={bar.onPower || 250}
offPower={bar.offPower || 120}
onLength={bar.onLength || 200}
offLength={bar.offLength || 200}
cadence={bar.cadence}
restingCadence={bar.restingCadence || 0}
ftp={ftp}
weight={weight}
sportType={sportType}
durationType={durationType}
pace={bar.pace || 0}
speed={calculateSpeed(bar.pace || 0)}
handleIntervalChange={(id: string, value: any) =>
handleOnChange(id, value)
}
handleIntervalClick={(id: string) => handleOnClick(id)}
selected={bar.id === actionId}
/>
);
const renderComment = (instruction: Instruction, index: number) => (
<Comment
key={instruction.id}
instruction={instruction}
durationType={durationType}
width={
durationType === "distance"
? parseInt(helpers.getWorkoutDistance(bars)) * 100
: helpers.getWorkoutLength(bars, durationType) / 3
}
onChange={(id: string, values: Instruction) =>
changeInstruction(id, values)
}
onClick={(id: string) =>
setSelectedInstruction(instructions.find((i) => i.id === id))
}
index={index}
/>
);
const renderRegistrationForm = () => {
switch (visibleForm) {
case "login":
return (
<LoginForm
login={setUser}
showSignup={() => setVisibleForm("signup")}
dismiss={() => setSavePopupVisibility(false)}
showForgotPassword={() => setVisibleForm("forgotPassword")}
/>
);
case "signup":
return (
<SignupForm
signUp={setUser}
showLogin={() => setVisibleForm("login")}
dismiss={() => setSavePopupVisibility(false)}
/>
);
case "forgotPassword":
return (
<ForgotPasswordForm
dismiss={() => setSavePopupVisibility(false)}
workoutId={id}
/>
);
case "updatePassword":
return (
<UpdatePasswordForm
dismiss={() => {
setVisibleForm("login");
setSavePopupVisibility(true);
}}
/>
);
default:
break;
}
};
function setPace(value: string, id: string) {
const index = bars.findIndex((bar) => bar.id === id);
if (index !== -1) {
const updatedArray = [...bars];
const element = [...updatedArray][index];
element.pace = parseInt(value);
if (durationType === "time") {
element.length =
(helpers.calculateDistance(
element.time,
calculateSpeed(element.pace || 0)
) *
1) /
(element.power || 1);
} else {
element.time =
(helpers.calculateTime(
element.length,
calculateSpeed(element.pace || 0)
) *
1) /
(element.power || 1);
}
setBars(updatedArray);
}
}
function getPace(id: string) {
const index = bars.findIndex((bar) => bar.id === id);
if (index !== -1) {
const element = [...bars][index];
return element.pace;
}
}
function switchSportType(newSportType: SportType) {
setSportType(newSportType);
setDurationType(newSportType === "bike" ? "time" : "distance");
}
function toggleTextEditor() {
if (bars.length > 0 && !textEditorIsVisible) {
if (
window.confirm(
"Editing a workout from the text editor will overwrite current workout"
)
)
setTextEditorIsVisible(!textEditorIsVisible);
} else {
setTextEditorIsVisible(!textEditorIsVisible);
}
}
function transformTextToWorkout(textValue: string) {
// reset each time
setBars([]);
setInstructions([]);
// 2 minutes block at 112% FTP
// 2 minutes block at 330 W
// 30 seconds block at ..
//console.log(textValue);
const workoutBlocks = textValue.split("\n");
workoutBlocks.forEach((workoutBlock) => {
if (workoutBlock.includes("steady")) {
// generate a steady state block
// extract watts
const powerInWatts = workoutBlock.match(/([0-9]\d*w)/);
const powerInWattsPerKg = workoutBlock.match(/([0-9]*.?[0-9]wkg)/);
const powerInPercentageFtp = workoutBlock.match(/([0-9]\d*%)/);
let power = powerInWatts ? parseInt(powerInWatts[0]) / ftp : 1;
power = powerInWattsPerKg
? (parseFloat(powerInWattsPerKg[0]) * weight) / ftp
: power;
power = powerInPercentageFtp
? parseInt(powerInPercentageFtp[0]) / 100
: power;
// extract duration in seconds
const durationInSeconds = workoutBlock.match(/([0-9]\d*s)/);
const durationInMinutes = workoutBlock.match(/([0-9]*:?[0-9][0-9]*m)/);
let duration = durationInSeconds && parseInt(durationInSeconds[0]);
duration = durationInMinutes
? parseInt(durationInMinutes[0].split(":")[0]) * 60 +
(parseInt(durationInMinutes[0].split(":")[1]) || 0)
: duration;
// extract cadence in rpm
const cadence = workoutBlock.match(/([0-9]\d*rpm)/);
const rpm = cadence ? parseInt(cadence[0]) : undefined;
// extract multiplier
// const multiplier = workoutBlock.match(/([0-9]\d*x)/)
// const nTimes = multiplier ? Array(parseInt(multiplier[0])) : Array(1)
// for (var i = 0; i < nTimes.length; i++)
addBar(power, duration || 300, rpm);
}
if (
workoutBlock.includes("ramp") ||
workoutBlock.includes("warmup") ||
workoutBlock.includes("cooldown")
) {
// generate a steady ramp block
// extract watts
const startPowerInWatts = workoutBlock.match(/([0-9]\d*w)/);
const startPowerInWattsPerKg = workoutBlock.match(/([0-9]*.?[0-9]wkg)/);
const startPowerInPercentageFtp = workoutBlock.match(/([0-9]\d*%)/);
let startPower = startPowerInWatts
? parseInt(startPowerInWatts[0]) / ftp
: 1;
startPower = startPowerInWattsPerKg
? (parseFloat(startPowerInWattsPerKg[0]) * weight) / ftp
: startPower;
startPower = startPowerInPercentageFtp
? parseInt(startPowerInPercentageFtp[0]) / 100
: startPower;
// extract watts
const endPowerInWatts = workoutBlock.match(/(-[0-9]\d*w)/);
const endPowerInWattsPerKg = workoutBlock.match(/(-[0-9]*.?[0-9]wkg)/);
const endPowerInPercentageFtp = workoutBlock.match(/-([0-9]\d*%)/);
let endPower = endPowerInWatts
? Math.abs(parseInt(endPowerInWatts[0])) / ftp
: 1;
endPower = endPowerInWattsPerKg
? (Math.abs(parseFloat(endPowerInWattsPerKg[0])) * weight) / ftp
: endPower;
endPower = endPowerInPercentageFtp
? Math.abs(parseInt(endPowerInPercentageFtp[0])) / 100
: endPower;
const durationInSeconds = workoutBlock.match(/([0-9]\d*s)/);
const durationInMinutes = workoutBlock.match(/([0-9]*:?[0-9][0-9]*m)/);
let duration = durationInSeconds && parseInt(durationInSeconds[0]);
duration = durationInMinutes
? parseInt(durationInMinutes[0].split(":")[0]) * 60 +
(parseInt(durationInMinutes[0].split(":")[1]) || 0)
: duration;
// extract cadence in rpm
const cadence = workoutBlock.match(/([0-9]\d*rpm)/);
const rpm = cadence ? parseInt(cadence[0]) : undefined;
addTrapeze(
startPower,
endPower,
duration || 300,
undefined,
undefined,
rpm
);
}
if (workoutBlock.includes("freeride")) {
const durationInSeconds = workoutBlock.match(/([0-9]\d*s)/);
const durationInMinutes = workoutBlock.match(/([0-9]*:?[0-9][0-9]*m)/);
let duration = durationInSeconds && parseInt(durationInSeconds[0]);
duration = durationInMinutes
? parseInt(durationInMinutes[0].split(":")[0]) * 60 +
(parseInt(durationInMinutes[0].split(":")[1]) || 0)
: duration;
// extract cadence in rpm
const cadence = workoutBlock.match(/([0-9]\d*rpm)/);
const rpm = cadence ? parseInt(cadence[0]) : undefined;
addFreeRide(duration || 600, rpm);
}
if (workoutBlock.includes("interval")) {
const multiplier = workoutBlock.match(/([0-9]\d*x)/);
const nTimes = multiplier ? parseInt(multiplier[0]) : 3;
const durationInSeconds = workoutBlock.match(/([0-9]\d*s)/);
const durationInMinutes = workoutBlock.match(/([0-9]*:?[0-9][0-9]*m)/);
let duration = durationInSeconds && parseInt(durationInSeconds[0]);
duration = durationInMinutes
? parseInt(durationInMinutes[0].split(":")[0]) * 60 +
(parseInt(durationInMinutes[0].split(":")[1]) || 0)
: duration;
const offDurationInSeconds = workoutBlock.match(/(-[0-9]\d*s)/);
const offDurationInMinutes = workoutBlock.match(
/(-[0-9]*:?[0-9][0-9]*m)/
);
let offDuration =
offDurationInSeconds && Math.abs(parseInt(offDurationInSeconds[0]));
offDuration = offDurationInMinutes
? Math.abs(parseInt(offDurationInMinutes[0].split(":")[0])) * 60 +
(parseInt(offDurationInMinutes[0].split(":")[1]) || 0)
: offDuration;
// extract watts
const startPowerInWatts = workoutBlock.match(/([0-9]\d*w)/);
const startPowerInWattsPerKg = workoutBlock.match(/([0-9]*.?[0-9]wkg)/);
const startPowerInPercentageFtp = workoutBlock.match(/([0-9]\d*%)/);
let startPower = startPowerInWatts
? parseInt(startPowerInWatts[0]) / ftp
: 1;
startPower = startPowerInWattsPerKg
? (parseFloat(startPowerInWattsPerKg[0]) * weight) / ftp
: startPower;
startPower = startPowerInPercentageFtp
? parseInt(startPowerInPercentageFtp[0]) / 100
: startPower;
// extract watts
const endPowerInWatts = workoutBlock.match(/(-[0-9]\d*w)/);
const endPowerInWattsPerKg = workoutBlock.match(/(-[0-9]*.?[0-9]wkg)/);
const endPowerInPercentageFtp = workoutBlock.match(/-([0-9]\d*%)/);
let endPower = endPowerInWatts
? Math.abs(parseInt(endPowerInWatts[0])) / ftp
: 0.5;
endPower = endPowerInWattsPerKg
? (Math.abs(parseFloat(endPowerInWattsPerKg[0])) * weight) / ftp
: endPower;
endPower = endPowerInPercentageFtp
? Math.abs(parseInt(endPowerInPercentageFtp[0])) / 100
: endPower;
// extract cadence in rpm
const cadence = workoutBlock.match(/([0-9]\d*rpm)/);
const rpm = cadence ? parseInt(cadence[0]) : undefined;
const restingCadence = workoutBlock.match(/(-[0-9]\d*rpm)/);
const restingRpm = restingCadence
? Math.abs(parseInt(restingCadence[0]))
: undefined;
addInterval(
nTimes,
duration || 30,
offDuration || 120,
startPower,
endPower,
rpm,
restingRpm
);
}
if (workoutBlock.includes("message")) {
// extract message
const message = workoutBlock.match(/["'](.*?)["']/);
const text = message ? message[0] : "";
const durationInSeconds = workoutBlock.match(/([0-9]\d*s)/);
const durationInMinutes = workoutBlock.match(/([0-9]*:?[0-9][0-9]*m)/);
let duration = durationInSeconds && parseInt(durationInSeconds[0]);
duration = durationInMinutes
? parseInt(durationInMinutes[0].split(":")[0]) * 60 +
(parseInt(durationInMinutes[0].split(":")[1]) || 0)
: duration;
addInstruction(text, duration || 0);
}
});
}
return (
// Adding tabIndex allows div element to receive keyboard events
<div className="container" onKeyDown={handleKeyPress} tabIndex={0}>
<Helmet>
<title>
{name
? `${name} - Zwift Workout Editor`
: "My Workout - Zwift Workout Editor"}
</title>
<meta name="description" content={description} />
<meta
property="og:title"
content={
name
? `${name} - Zwift Workout Editor`
: "My Workout - Zwift Workout Editor"
}
/>
<meta property="og:description" content={description} />
<link
rel="canonical"
href={`https://www.zwiftworkout.com/editor/${id}`}
/>
<meta
property="og:url"
content={`https://www.zwiftworkout.com/editor/${id}`}
/>
</Helmet>
{message?.visible && (
<div className={`message ${message.class}`}>
{message.text}
<button
className="close"
onClick={() => setMessage({ visible: false })}
>
<FontAwesomeIcon icon={faTimesCircle} size="lg" fixedWidth />
</button>
</div>
)}
{showWorkouts && (
<Popup width="500px" dismiss={() => setShowWorkouts(false)}>
{user ? <Workouts userId={user.uid} /> : renderRegistrationForm()}
</Popup>
)}
{selectedInstruction && (
<EditComment
instruction={selectedInstruction}
onChange={(id: string, values: Instruction) => {
changeInstruction(id, values);
setSelectedInstruction(undefined);
}}
dismiss={() => setSelectedInstruction(undefined)}
onDelete={(id: string) => {
deleteInstruction(id);
setSelectedInstruction(undefined);
}}
/>
)}
{savePopupIsVisile && (
<Popup width="500px" dismiss={() => setSavePopupVisibility(false)}>
{user ? (
<SaveForm
name={name}
description={description}
author={author}
tags={tags}
onNameChange={setName}
onDescriptionChange={setDescription}
onAuthorChange={setAuthor}
onTagsChange={setTags}
onSave={() => {
save();
setSavePopupVisibility(false);
}}
onDismiss={() => setSavePopupVisibility(false)}
onLogout={logout}
/>
) : (
renderRegistrationForm()
)}
</Popup>
)}
{sharePopupIsVisile && (
<Popup width="500px" dismiss={() => setSharePopupVisibility(false)}>
<ShareForm id={id} onDismiss={() => setSharePopupVisibility(false)} />
</Popup>
)}
<div className="info">
<div className="title">
<h1>{name}</h1>
<div className="description">{description}</div>
<p>{author ? `by ${author}` : ""}</p>
</div>
<div className="workout">
<div className="form-input">
<label>Workout Time</label>
<input
className="textInput"
value={helpers.formatDuration(
helpers.getWorkoutLength(bars, durationType)
)}
disabled
/>
</div>
{sportType === "run" && (
<div className="form-input">
<label>Workout Distance</label>
<input
className="textInput"
value={helpers.getWorkoutDistance(bars)}
disabled
/>
</div>
)}
{sportType === "bike" && (
<div className="form-input">
<label title="Training Load">Training Load</label>
<input
className="textInput"
value={helpers.getStressScore(bars, ftp)}
disabled
/>
</div>
)}
{sportType === "run" && (
<LeftRightToggle<"time", "distance">
label="Duration Type"
leftValue="time"
rightValue="distance"
leftIcon={faClock}
rightIcon={faRuler}
selected={durationType}
onChange={setDurationType}
/>
)}
<LeftRightToggle<"bike", "run">
label="Sport Type"
leftValue="bike"
rightValue="run"
leftIcon={faBiking}
rightIcon={faRunning}
selected={sportType}
onChange={switchSportType}
/>
</div>
</div>
{sportType === "run" && (
<RunningTimesEditor times={runningTimes} onChange={setRunningTimes} />
)}
{textEditorIsVisible && sportType === "bike" && (
<div className="text-editor">
<textarea
onChange={(e) => transformTextToWorkout(e.target.value)}
rows={10}
spellCheck={false}
className="text-editor-textarea"
placeholder="Add one block per line here: steady 3.0wkg 30s"
></textarea>
<div className="text-editor-instructions">
<h2>Instructions</h2>
<p>
Every row correspond to a workout block. Scroll down to see some
examples.
</p>
<h3>Blocks</h3>
<p>
<span>steady</span> <span>warmup</span> <span>cooldown</span>{" "}
<span>ramp</span> <span>intervals</span> <span>freeride</span>{" "}
<span>message</span>
</p>
<h3>Time</h3>
<p>
<span>30s</span> or <span>0:30m</span>
</p>
<h3>Power</h3>
<p>
<span>250w</span> or <span>3.0wkg</span> or <span>75%</span> (FTP)
</p>
<h3>Cadence</h3>
<p>
<span>120rpm</span>
</p>
<h2>Examples</h2>
<h3>Steady block</h3>
<p>
<code>steady 3.0wkg 30s</code>
<code>steady 120w 10m 85rpm</code>
</p>
<h3>Warmup / Cooldown / Ramp block</h3>
<p>
<code>warmup 2.0wkg-3.5wkg 10m</code>
<code>cooldown 180w-100w 5m 110rpm</code>
</p>
<h3>Intervals</h3>
<p>
<code>interval 10x 30s-30s 4.0wkg-1.0wkg 110rpm-85rpm</code>
<code>interval 3x 1:00m-5:00m 300w-180w</code>
</p>
<h3>Free Ride</h3>
<p>
<code>freeride 10m 85rpm</code>
</p>
<h3>Text Event</h3>
<p>
<code>message "Get ready to your first set!" 30s</code>
<code>message "Last one!" 20:00m</code>
</p>
</div>
</div>
)}
<div id="editor" className="editor">
{actionId && (
<div className="actions">
<button onClick={() => moveLeft(actionId)} title="Move Left">
<FontAwesomeIcon icon={faArrowLeft} size="lg" fixedWidth />
</button>
<button onClick={() => moveRight(actionId)} title="Move Right">
<FontAwesomeIcon icon={faArrowRight} size="lg" fixedWidth />
</button>
<button onClick={() => removeBar(actionId)} title="Delete">
<FontAwesomeIcon icon={faTrash} size="lg" fixedWidth />
</button>
<button onClick={() => duplicateBar(actionId)} title="Duplicate">
<FontAwesomeIcon icon={faCopy} size="lg" fixedWidth />
</button>
{sportType === "run" && (
<select
name="pace"
value={getPace(actionId)}
onChange={(e) => setPace(e.target?.value, actionId)}
className="selectInput"
>
<option value="0">1 Mile Pace</option>
<option value="1">5K Pace</option>
<option value="2">10K Pace</option>
<option value="3">Half Marathon Pace</option>
<option value="4">Marathon Pace</option>
</select>
)}
</div>
)}
<div className="canvas" ref={canvasRef}>
{actionId && (
<div
className="fader"
style={{ width: canvasRef.current?.scrollWidth }}
onClick={() => setActionId(undefined)}
></div>
)}
<div className="segments" ref={segmentsRef}>
{bars.map((bar) => {
if (bar.type === "bar") {
return renderBar(bar);
} else if (bar.type === "trapeze") {
return renderTrapeze(bar);
} else if (bar.type === "freeRide") {
return renderFreeRide(bar);
} else if (bar.type === "interval") {
return renderInterval(bar);
} else {
return false;
}
})}
</div>
<div className="slider">
{instructions.map((instruction, index) =>
renderComment(instruction, index)
)}
</div>
{durationType === "time" ? (
<TimeAxis width={segmentsWidth} />
) : (
<DistanceAxis width={segmentsWidth} />
)}
</div>
<ZoneAxis />
</div>
<div className="cta">
{sportType === "bike" ? (
<div>
<ReactTooltip effect="solid" />
<button
className="btn btn-square"
onClick={() => toggleTextEditor()}
style={{ backgroundColor: "palevioletred" }}
data-tip="New! Workout text editor!"
>
<FontAwesomeIcon icon={faPen} fixedWidth />
</button>
<button
className="btn btn-square"
onClick={() => addBar(0.5)}
style={{ backgroundColor: Colors.GRAY }}
>
Z1
</button>
<button
className="btn btn-square"
onClick={() => addBar(Zones.Z2.min)}
style={{ backgroundColor: Colors.BLUE }}
>
Z2
</button>
<button
className="btn btn-square"
onClick={() => addBar(Zones.Z3.min)}
style={{ backgroundColor: Colors.GREEN }}
>
Z3
</button>
<button
className="btn btn-square"
onClick={() => addBar(Zones.Z4.min)}
style={{ backgroundColor: Colors.YELLOW }}
>
Z4
</button>
<button
className="btn btn-square"
onClick={() => addBar(Zones.Z5.min)}
style={{ backgroundColor: Colors.ORANGE }}
>
Z5
</button>
<button
className="btn btn-square"
onClick={() => addBar(Zones.Z6.min)}
style={{ backgroundColor: Colors.RED }}
>
Z6
</button>
</div>
) : (
<button
className="btn"
onClick={() => addBar(1, 300, 0, 0, 1000)}
style={{ backgroundColor: Colors.WHITE }}
>
<SteadyLogo className="btn-icon" /> Steady Pace
</button>
)}
<button
className="btn"
onClick={() => addTrapeze(0.25, 0.75)}
style={{ backgroundColor: Colors.WHITE }}
>
<WarmupLogo className="btn-icon" /> Warm up
</button>
<button
className="btn"
onClick={() => addTrapeze(0.75, 0.25)}
style={{ backgroundColor: Colors.WHITE }}
>
<WarmdownLogo className="btn-icon" /> Cool down
</button>
<button
className="btn"
onClick={() => addInterval()}
style={{ backgroundColor: Colors.WHITE }}
>
<IntervalLogo className="btn-icon" /> Interval
</button>
<button
className="btn"
onClick={() => addFreeRide()}
style={{ backgroundColor: Colors.WHITE }}
>
<FontAwesomeIcon
icon={sportType === "bike" ? faBicycle : faRunning}
size="lg"
fixedWidth
/>{" "}
Free {sportType === "bike" ? "Ride" : "Run"}
</button>
<button
className="btn"
onClick={() => addInstruction()}
style={{ backgroundColor: Colors.WHITE }}
>
<FontAwesomeIcon icon={faComment} size="lg" fixedWidth /> Text Event
</button>
{sportType === "bike" && (
<div className="form-input">
<label htmlFor="ftp">FTP (W)</label>
<input
className="textInput"
type="number"
name="ftp"
value={ftp}
onChange={(e) => setFtp(parseInt(e.target.value))}
/>
</div>
)}
{sportType === "bike" && (
<div className="form-input">
<label htmlFor="weight">Body Weight (Kg)</label>
<input
className="textInput"
type="number"
name="weight"
value={weight}
onChange={(e) => setWeight(parseInt(e.target.value))}
/>
</div>
)}
<button
className="btn"
onClick={() => {
if (
window.confirm("Are you sure you want to create a new workout?")
)
newWorkout();
}}
>
<FontAwesomeIcon icon={faFile} size="lg" fixedWidth /> New
</button>
<button className="btn" onClick={() => saveWorkout()}>
<FontAwesomeIcon icon={faSave} size="lg" fixedWidth /> Save
</button>
<button
className="btn"
onClick={() => {
if (window.confirm("Are you sure you want to delete this workout?"))
deleteWorkout();
}}
>
<FontAwesomeIcon icon={faTrash} size="lg" fixedWidth /> Delete
</button>
<button className="btn" onClick={() => downloadWorkout()}>
<FontAwesomeIcon icon={faDownload} size="lg" fixedWidth /> Download
</button>
<input
accept=".xml,.zwo"
id="contained-button-file"
type="file"
style={{ display: "none" }}
onChange={(e) => handleUpload(e.target.files![0])}
/>
<button
className="btn"
onClick={() =>
document.getElementById("contained-button-file")!.click()
}
>
<FontAwesomeIcon icon={faUpload} size="lg" fixedWidth /> Upload
</button>
<button className="btn" onClick={() => setShowWorkouts(true)}>
<FontAwesomeIcon icon={faList} size="lg" fixedWidth /> Workouts
</button>
<button className="btn" onClick={() => shareWorkout()}>
<FontAwesomeIcon icon={faShareAlt} size="lg" fixedWidth /> Share
</button>
</div>
<Footer />
</div>
);
}
Example #14
Source File: ResolverSkylink.tsx From argo-react with MIT License | 4 votes |
ResolverSkylink = () => {
const { projectLoading, selectedProject, orgLoading } =
useContext<IStateModel>(StateContext);
const [showCreatePopup, setShowCreatePopup] = useState<boolean>(false);
const [showUpdatePopup, setShowUpdatePopup] = useState<boolean>(false);
const [showRemovePopup, setShowRemovePopup] = useState<boolean>(false);
const [isCopied, setIsCopied] = useState<boolean>(false);
const [resolverSkylinkLoading, setResolverSkylinkLoading] =
useState<boolean>(false);
const [resolverSkylinks, setResolverSkylinks] = useState<IResolverSkylink[]>([]);
const [selectedResolver, setSelectedResolver] = useState<IResolverSkylink>();
const componentIsMounted = useRef<boolean>(true);
useEffect(() => {
if (selectedProject && !projectLoading) {
if (componentIsMounted.current) {
const resolverSkylinks: IResolverSkylink[] =
selectedProject.resolverSkylinks;
setResolverSkylinks(resolverSkylinks);
setResolverSkylinkLoading(false);
}
} else {
if (projectLoading) {
setResolverSkylinkLoading(true);
} else {
setResolverSkylinkLoading(false);
}
}
}, [selectedProject, projectLoading]);
return (
<div className="ResolverSkylink">
<ProjectTopCard />
<div className="skylinks-container">
<div className="skylinks-details">
<div className="skylinks-header">
<span>Resolver Skylinks</span>
<button
type="button"
className="primary-button"
disabled={projectLoading || orgLoading}
onClick={(e) => setShowCreatePopup(true)}
>
Generate Resolver Skylinks
</button>
<Popup
trigger={<></>}
position="center center"
open={showCreatePopup}
className="popup-container"
modal
>
<GenerateResolverSkylink
type="create"
close={() => setShowCreatePopup(false)}
/>
</Popup>
<Popup
trigger={<></>}
position="center center"
open={showUpdatePopup}
className="popup-container"
modal
>
<GenerateResolverSkylink
type="update"
resolver={selectedResolver}
close={() => setShowUpdatePopup(false)}
/>
</Popup>
<Popup
trigger={<></>}
position="center center"
open={showRemovePopup}
className="popup-container"
modal
>
<GenerateResolverSkylink
type="remove"
resolver={selectedResolver}
close={() => setShowRemovePopup(false)}
/>
</Popup>
</div>
<div className="skylinks-body">
<div className="table">
<div className="thead">
<div className="tr">
<div className="th">Name</div>
<div className="th">Resolver Skylink</div>
<div className="th">Target Skylink</div>
<div className="th">Last Updated</div>
<div className="th"></div>
</div>
</div>
{!resolverSkylinkLoading ? (
<div className="tbody">
{resolverSkylinks.length > 0 ? (
resolverSkylinks.map(
(resolverSkylink: IResolverSkylink, index: number) => (
<div className="tr" key={index}>
<div className="tr-first">
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-title">
{resolverSkylink.name}
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<a
className="skylinks-links"
href={`https://siasky.net/${resolverSkylink.resolverSkylink}`}
target="_blank"
rel="noopener noreferrer"
>
sia://{resolverSkylink.resolverSkylink}
</a>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<a
className="skylinks-links"
href={`https://siasky.net/${resolverSkylink.resolverSkylink}`}
target="_blank"
rel="noopener noreferrer"
>
sia://{resolverSkylink.targetSkylink}
</a>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-text">
{moment(resolverSkylink.updatedAt).format(
"DD-MM-YYYY hh:mm A",
)}
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div
className={`trash-icon-container ${
projectLoading || orgLoading
? "icon-disabled"
: ""
}`}
onClick={(e) => {
setSelectedResolver(resolverSkylink);
setShowUpdatePopup(true);
}}
>
<FontAwesomeIcon
icon={faEdit}
className="trash-icon"
></FontAwesomeIcon>
</div>
<div
className={`trash-icon-container ${
projectLoading || orgLoading
? "icon-disabled"
: ""
}`}
onClick={(e) => {
setSelectedResolver(resolverSkylink);
setShowRemovePopup(true);
}}
>
<FontAwesomeIcon
icon={faTrash}
className="trash-icon"
></FontAwesomeIcon>
</div>
</div>
</div>
</div>
<div className="tr-second">
<div
className="badge-container"
onClick={() =>
window.open(
`https://homescreen.hns.siasky.net/#/skylink/${resolverSkylink.resolverSkylink}`,
"_blank",
)
}
>
<img
src="https://img.shields.io/badge/Skynet-Add%20To%20Homescreen-00c65e?style=for-the-badge&labelColor=0d0d0d&logo=data:image/png;base64,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"
alt="homescreen badge"
/>
</div>
<div
className="copy-badge-container"
onClick={() => {
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
navigator.clipboard.writeText(
`[![Add to Homescreen](https://img.shields.io/badge/Skynet-Add%20To%20Homescreen-00c65e?style=for-the-badge&labelColor=0d0d0d&logo=data:image/png;base64,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)](https://homescreen.hns.siasky.net/#/skylink/${resolverSkylink.resolverSkylink})`,
);
}}
>
<div className="copy-badge-text">
{isCopied ? "Copied" : "Copy"} Homescreen Badge Code
</div>
<div className="clipboard-icon-container">
<FontAwesomeIcon
icon={faClipboard}
className="clipboard-icon"
></FontAwesomeIcon>
</div>
</div>
</div>
</div>
),
)
) : (
<div className="tr" key={1}>
<div className="tr-first no-item">
No resolver skylinks to show
</div>
</div>
)}
</div>
) : (
<div className="tbody">
<div className="tr">
<div className="tr-first">
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-title">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-title">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-title">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-text">
<Skeleton width={80} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-text">
<Skeleton width={80} duration={2} />
</div>
</div>
</div>
</div>
</div>
<div className="tr">
<div className="tr-first">
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-title">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-title">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-title">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-text">
<Skeleton width={80} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="skylinks-td-container">
<div className="skylinks-text">
<Skeleton width={80} duration={2} />
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}
Example #15
Source File: ArtifactFilter.tsx From genshin-optimizer with MIT License | 4 votes |
export function ArtifactRedButtons({ artifactIds, filterOption }:
{ artifactIds: string[], filterOption: FilterOption }) {
const { t } = useTranslation(["artifact", "ui"]);
const { database } = useContext(DatabaseContext)
const { numDelete, numUnequip, numExclude, numInclude, numUnlock, numLock } = useMemo(() => {
const artifacts = artifactIds.map(id => database._getArt(id)) as ICachedArtifact[]
const numUnlock = artifacts.reduce((a, art) => a + (art.lock ? 0 : 1), 0)
const numLock = artifacts.length - numUnlock
const numDelete = numUnlock
const numUnequip = artifacts.reduce((a, art) => a + (art.location ? 1 : 0), 0)
const numExclude = artifacts.reduce((a, art) => a + (art.exclude ? 1 : 0), 0)
const numInclude = artifacts.length - numExclude
return { numDelete, numUnequip, numExclude, numInclude, numUnlock, numLock }
}, [artifactIds, database])
const unequipArtifacts = () =>
window.confirm(`Are you sure you want to unequip ${numUnequip} artifacts currently equipped on characters?`) &&
artifactIds.map(id => database.setArtLocation(id, ""))
const deleteArtifacts = () =>
window.confirm(`Are you sure you want to delete ${numDelete} artifacts?`) &&
artifactIds.map(id => !database._getArt(id)?.lock && database.removeArt(id))
const excludeArtifacts = () =>
window.confirm(`Are you sure you want to exclude ${numInclude} artifacts from build generations?`) &&
artifactIds.map(id => database.updateArt({ exclude: true }, id))
const includeArtifacts = () =>
window.confirm(`Are you sure you want to include ${numExclude} artifacts in build generations?`) &&
artifactIds.map(id => database.updateArt({ exclude: false }, id))
const lockArtifacts = () =>
window.confirm(`Are you sure you want to lock ${numUnlock} artifacts?`) &&
artifactIds.map(id => database.updateArt({ lock: true }, id))
const unlockArtifacts = () =>
window.confirm(`Are you sure you want to unlock ${numLock} artifacts?`) &&
artifactIds.map(id => database.updateArt({ lock: false }, id))
return <Grid container spacing={1} alignItems="center">
<Grid item xs={12} sm={6} md={3}>
<Button fullWidth color="error" disabled={!numUnequip} onClick={unequipArtifacts} startIcon={<FontAwesomeIcon icon={faUserSlash} />}>
<Trans t={t} i18nKey="button.unequipArtifacts" >Unequip Artifacts</Trans>
<SqBadge sx={{ ml: 1 }} color={numUnequip ? "success" : "secondary"}>{numUnequip}</SqBadge>
</Button>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Button fullWidth color="error" disabled={!numDelete} onClick={deleteArtifacts} startIcon={<FontAwesomeIcon icon={faTrash} />}>
<Trans t={t} i18nKey="button.deleteArtifacts" >Delete Artifacts</Trans>
<SqBadge sx={{ ml: 1 }} color={numDelete ? "success" : "secondary"}>{numDelete}</SqBadge>
</Button>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Button fullWidth color="error" disabled={!numInclude} onClick={excludeArtifacts} startIcon={<FontAwesomeIcon icon={faBan} />}>
<Trans t={t} i18nKey="button.excludeArtifacts" >Exclude Artifacts</Trans>
<SqBadge sx={{ ml: 1 }} color={numInclude ? "success" : "secondary"}>{numInclude}</SqBadge>
</Button>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Button fullWidth color="error" disabled={!numExclude} onClick={includeArtifacts} startIcon={<FontAwesomeIcon icon={faChartLine} />}>
<Trans t={t} i18nKey="button.includeArtifacts" >Include Artifacts</Trans>
<SqBadge sx={{ ml: 1 }} color={numExclude ? "success" : "secondary"}>{numExclude}</SqBadge>
</Button>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Button fullWidth color="error" disabled={!numLock} onClick={unlockArtifacts} startIcon={<LockOpen />}>
<Trans t={t} i18nKey="button.unlockrtifacts" >Unlock Artifacts</Trans>
<SqBadge sx={{ ml: 1 }} color={numLock ? "success" : "secondary"}>{numLock}</SqBadge>
</Button>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Button fullWidth color="error" disabled={!numUnlock} onClick={lockArtifacts} startIcon={<Lock />}>
<Trans t={t} i18nKey="button.lockArtifacts" >Lock Artifacts</Trans>
<SqBadge sx={{ ml: 1 }} color={numUnlock ? "success" : "secondary"}>{numUnlock}</SqBadge>
</Button>
</Grid>
<Grid item xs={12} sm={12} md={6} display="flex" justifyContent="space-around">
<Typography variant="caption" color="text.secondary"><Trans t={t} i18nKey="buttonHint">Note: the red buttons above only applies to <b>currently filtered artifacts</b></Trans></Typography>
</Grid>
</Grid>
}
Example #16
Source File: Members.tsx From argo-react with MIT License | 4 votes |
Members = () => {
const history = useHistory();
const { selectedOrg, orgLoading, user } = useContext<IStateModel>(StateContext);
const [memberLoading, setMemberLoading] = useState<boolean>(false);
const [members, setMembers] = useState<IMemberModel[]>([]);
const [invitedMembers, setInvitedMembers] = useState<IInviteMemberModel[]>([]);
const { fetchUser } = useContext<IActionModel>(ActionContext);
const componentIsMounted = useRef(true);
useEffect(() => {
if (selectedOrg && !orgLoading) {
if (componentIsMounted.current) {
const members: IMemberModel[] = selectedOrg.users.map((user: IUser) => ({
name: user.argoProfile.name,
email: user.argoProfile.email,
avatar: user.argoProfile.avatar,
username: user.argoProfile.username,
id: user._id,
}));
const invitedMembers: IInviteMemberModel[] = selectedOrg.invitedMembers.map(
(index: IUserInvite) => ({
email: index.userEmail,
status: index.status,
link: index.link,
id: index._id,
}),
);
setInvitedMembers(invitedMembers);
setMembers(members);
setMemberLoading(false);
}
} else {
if (orgLoading) {
setMemberLoading(true);
} else {
setMemberLoading(false);
}
}
}, [selectedOrg, orgLoading]);
const deleteInvitedUser = (userId: string) => {
ApiService.deleteInvite(userId).subscribe((result) => {
fetchUser();
});
};
const deleteMember = (userId: string) => {
ApiService.deleteMember(selectedOrg?._id!, userId).subscribe((result) => {
fetchUser();
});
};
useEffect(() => {
return () => {
componentIsMounted.current = false;
};
}, []);
return (
<div className="Members">
<div className="members-container">
<div className="members-details">
<div className="members-header">
<span>Organisation members</span>
<button
type="button"
className="primary-button"
disabled={orgLoading}
onClick={(e) => history.push("/dashboard/members/new")}
>
Add Members
</button>
</div>
<div className="members-body">
<div className="table">
<div className="thead">
<div className="tr">
<div className="th"></div>
<div className="th">User</div>
<div className="th">Email</div>
<div className="th"></div>
</div>
</div>
{!memberLoading ? (
<div className="tbody">
{members.map((member: IMemberModel, index: number) => (
<div className="tr" key={index}>
<div className="td">
<div className="avatar-container">
<LazyLoadedImage height={32} once>
<img
src={member.avatar}
alt="avatar"
className="profile-avatar"
height={32}
width={32}
loading="lazy"
/>
</LazyLoadedImage>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-email">{member.name}</div>
<div className="user-username">{member.username}</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-username">
{member.email || "N.A"}
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="trash-icon-container">
{member.id !== user?._id && (
<FontAwesomeIcon
icon={faTrash}
className="trash-icon"
onClick={() => deleteMember(member.id)}
></FontAwesomeIcon>
)}
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="tbody">
<div className="tr">
<div className="td">
<div className="avatar-container">
<Skeleton
circle={true}
height={32}
width={32}
duration={2}
/>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-email">
<Skeleton width={150} duration={2} />
</div>
<div className="user-username">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-username">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-username">
<Skeleton width={20} duration={2} />
</div>
</div>
</div>
</div>
<div className="tr">
<div className="td">
<div className="avatar-container">
<Skeleton
circle={true}
height={32}
width={32}
duration={2}
/>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-email">
<Skeleton width={150} duration={2} />
</div>
<div className="user-username">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-username">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-username">
<Skeleton width={20} duration={2} />
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
<div className="members-container">
<div className="members-details">
<div className="members-header">
<span>Invited members</span>
</div>
<div className="members-body">
<div className="invite-table">
<div className="thead">
<div className="tr">
<div className="th">Invited user</div>
<div className="th">Status</div>
<div className="th"></div>
</div>
</div>
{!memberLoading ? (
<div className="tbody">
{invitedMembers.length > 0 ? (
invitedMembers.map((member: IInviteMemberModel, index: any) => (
<div className="tr" key={index}>
<div className="td">
<div className="user-container">
<div className="user-email">{member.email || "N.A"}</div>
<div className="user-username-message">
Awaiting user's response
</div>
</div>
</div>
<div className="td">
<div className="invite-user-container">
<div className="invite-user-username">
{member.status || "N.A"}
</div>
<div className="clipboard-icon-container">
<FontAwesomeIcon
onClick={() => {
navigator.clipboard.writeText(member.link);
}}
icon={faClipboard}
className="clipboard-icon"
></FontAwesomeIcon>
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="trash-icon-container">
<FontAwesomeIcon
icon={faTrash}
className="trash-icon"
onClick={() => deleteInvitedUser(member.id)}
></FontAwesomeIcon>
</div>
</div>
</div>
</div>
))
) : (
<div className="tr no-item" key={1}>
No invited members to show
</div>
)}
</div>
) : (
<div className="tbody">
<div className="tr">
<div className="td">
<div className="user-container">
<div className="user-email">
<Skeleton width={150} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-username">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-username">
<Skeleton width={20} duration={2} />
</div>
</div>
</div>
</div>
<div className="tr">
<div className="td">
<div className="user-container">
<div className="user-email">
<Skeleton width={150} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-username">
<Skeleton width={100} duration={2} />
</div>
</div>
</div>
<div className="td">
<div className="user-container">
<div className="user-username">
<Skeleton width={20} duration={2} />
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}
Example #17
Source File: ColumnTitle.tsx From knboard with MIT License | 4 votes |
ColumnTitle = ({ id, title, tasksCount, ...props }: Props) => {
const dispatch = useDispatch();
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
null
);
const [pendingTitle, setPendingTitle] = useState<string>(title);
const [editing, setEditing] = useState<boolean>(false);
const titleTextAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (!editing && title === pendingTitle) {
titleTextAreaRef?.current?.blur();
}
}, [pendingTitle, editing]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.keyCode === Key.Enter) {
e.preventDefault();
if (pendingTitle.length > 0) {
titleTextAreaRef?.current?.blur();
}
}
if (e.keyCode === Key.Escape) {
e.preventDefault();
setPendingTitle(title);
setEditing(false);
// blur via useEffect
}
};
const handleSave = () => {
if (editing && pendingTitle.length > 0) {
setEditing(false);
if (pendingTitle !== title) {
dispatch(patchColumn({ id, fields: { title: pendingTitle } }));
}
}
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPendingTitle(e.target.value);
};
const handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => {
e.target.select();
};
const handleOptionsClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleOptionsClose = () => {
setAnchorEl(null);
};
const handleDelete = () => {
if (
window.confirm(
"Are you sure? Deleting the column will also delete related tasks and this cannot be undone."
)
) {
dispatch(deleteColumn(id));
handleOptionsClose();
}
};
const open = Boolean(anchorEl);
const popoverId = open ? `col-${id}options-popover` : undefined;
return (
<Container {...props}>
{editing ? (
<InputTitle>
<TextareaAutosize
ref={titleTextAreaRef}
value={pendingTitle}
onChange={handleChange}
onBlur={handleSave}
onKeyDown={handleKeyDown}
data-testid="column-title-textarea"
onFocus={handleFocus}
autoFocus
/>
</InputTitle>
) : (
<RegularTitle onClick={() => setEditing(true)}>
{pendingTitle}
</RegularTitle>
)}
<Extra>
<InnerExtra>
<Count>{tasksCount}</Count>
<Button
onClick={handleOptionsClick}
data-testid="col-options"
css={css`
margin-left: 0.25rem;
min-width: 0;
padding: 2px 8px;
height: 22px;
`}
>
<FontAwesomeIcon icon={faEllipsisV} />
</Button>
</InnerExtra>
<Popover
id={popoverId}
open={open}
anchorEl={anchorEl}
onClose={handleOptionsClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<OptionsContent>
<Button
startIcon={<FontAwesomeIcon fixedWidth icon={faTrash} />}
onClick={handleDelete}
data-testid="delete-column"
size="small"
css={css`
font-size: 12px;
font-weight: bold;
color: ${ACTION_G};
`}
>
Delete column
</Button>
</OptionsContent>
</Popover>
</Extra>
</Container>
);
}
Example #18
Source File: EditTaskDialog.tsx From knboard with MIT License | 4 votes |
EditTaskDialog = () => {
const theme = useTheme();
const dispatch = useDispatch();
const columns = useSelector(selectAllColumns);
const labels = useSelector(selectAllLabels);
const labelsById = useSelector(selectLabelEntities);
const columnsById = useSelector(selectColumnsEntities);
const tasksByColumn = useSelector((state: RootState) => state.task.byColumn);
const taskId = useSelector((state: RootState) => state.task.editDialogOpen);
const tasksById = useSelector((state: RootState) => state.task.byId);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [editingDescription, setEditingDescription] = useState(false);
const titleTextAreaRef = useRef<HTMLTextAreaElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<MdEditor>(null);
const cancelRef = useRef<HTMLButtonElement>(null);
const xsDown = useMediaQuery(theme.breakpoints.down("xs"));
const open = taskId !== null;
useEffect(() => {
if (taskId && tasksById[taskId]) {
setDescription(tasksById[taskId].description);
setTitle(tasksById[taskId].title);
}
}, [open, taskId]);
const handleSaveTitle = () => {
if (taskId) {
dispatch(patchTask({ id: taskId, fields: { title } }));
}
};
const handleSaveDescription = () => {
if (taskId) {
dispatch(patchTask({ id: taskId, fields: { description } }));
setEditingDescription(false);
}
};
const handleCancelDescription = () => {
if (taskId && tasksById[taskId]) {
setDescription(tasksById[taskId].description);
setEditingDescription(false);
}
};
useEffect(() => {
const handleClickOutside = (event: any) => {
if (
wrapperRef.current &&
!wrapperRef.current.contains(event.target) &&
cancelRef.current &&
!cancelRef.current?.contains(event.target)
) {
handleSaveDescription();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [wrapperRef, taskId, description]);
useEffect(() => {
if (editingDescription && editorRef && editorRef.current) {
editorRef.current.setSelection({
start: 0,
end: description.length,
});
}
}, [editingDescription]);
const findTaskColumnId = () => {
for (const columnId in tasksByColumn) {
for (const id of tasksByColumn[columnId]) {
if (id === taskId) {
return columnId;
}
}
}
return null;
};
const columnId = findTaskColumnId();
if (!taskId || !tasksById[taskId] || !columnId) {
return null;
}
const task = tasksById[taskId];
const column = columnsById[columnId];
const handleEditorKeyDown = (e: React.KeyboardEvent) => {
if (e.keyCode == Key.Enter && e.metaKey) {
handleSaveDescription();
}
if (e.keyCode === Key.Escape) {
// Prevent propagation from reaching the Dialog
e.stopPropagation();
handleCancelDescription();
}
};
const handleTitleKeyDown = (e: React.KeyboardEvent) => {
if (e.keyCode === Key.Enter) {
e.preventDefault();
titleTextAreaRef?.current?.blur();
}
if (e.keyCode === Key.Escape) {
// Prevent propagation from reaching the Dialog
e.stopPropagation();
}
};
const handleClose = () => {
dispatch(setEditDialogOpen(null));
setEditingDescription(false);
};
const handleTitleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setTitle(e.target.value);
};
const handleColumnChange = (_: any, value: IColumn | null) => {
if (!column || !value || column.id === value.id) {
return;
}
const current: Id[] = [...tasksByColumn[column.id]];
const next: Id[] = [...tasksByColumn[value.id]];
const currentId = current.indexOf(task.id);
const newPosition = 0;
// remove from original
current.splice(currentId, 1);
// insert into next
next.splice(newPosition, 0, task.id);
const updatedTasksByColumn: TasksByColumn = {
...tasksByColumn,
[column.id]: current,
[value.id]: next,
};
dispatch(updateTasksByColumn(updatedTasksByColumn));
handleClose();
};
const handlePriorityChange = (_: any, priority: Priority | null) => {
if (priority) {
dispatch(patchTask({ id: taskId, fields: { priority: priority.value } }));
}
};
const handleNotImplemented = () => {
dispatch(createInfoToast("Not implemented yet ?"));
};
const handleDelete = () => {
if (window.confirm("Are you sure? Deleting a task cannot be undone.")) {
dispatch(deleteTask(task.id));
handleClose();
}
};
const handleDescriptionClick = () => {
setEditingDescription(true);
};
const handleEditorChange = ({ text }: any) => {
setDescription(text);
};
const handleLabelsChange = (newLabels: Label[]) => {
dispatch(
patchTask({
id: taskId,
fields: { labels: newLabels.map((label) => label.id) },
})
);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
// don't listen for input when inputs are focused
if (
document.activeElement instanceof HTMLInputElement ||
document.activeElement instanceof HTMLTextAreaElement
) {
return;
}
if (e.key === "Backspace" && e.metaKey) {
handleDelete();
}
if (e.key === "Escape" && e.metaKey) {
handleClose();
}
if (e.key === "l" && e.metaKey) {
e.preventDefault();
handleNotImplemented();
}
};
return (
<Dialog
open={open}
onClose={handleClose}
onKeyDown={handleKeyDown}
fullWidth
keepMounted={false}
fullScreen={xsDown}
css={css`
.MuiDialog-paper {
max-width: 920px;
}
`}
>
<Content theme={theme}>
<Close onClose={handleClose} />
<Main>
<Header>id: {task.id}</Header>
<Title>
<FontAwesomeIcon icon={faArrowUp} />
<TextareaAutosize
ref={titleTextAreaRef}
value={title}
onChange={handleTitleChange}
onBlur={handleSaveTitle}
onKeyDown={handleTitleKeyDown}
data-testid="task-title"
/>
</Title>
<DescriptionHeader>
<FontAwesomeIcon icon={faAlignLeft} />
<h3>Description</h3>
</DescriptionHeader>
<Description
key={`${taskId}${editingDescription}`}
data-testid="task-description"
>
<EditorWrapper
onDoubleClick={
editingDescription ? undefined : handleDescriptionClick
}
editing={editingDescription}
ref={wrapperRef}
theme={theme}
onKeyDown={handleEditorKeyDown}
>
<MdEditor
ref={editorRef}
plugins={MD_EDITOR_PLUGINS}
config={
editingDescription ? MD_EDITING_CONFIG : MD_READ_ONLY_CONFIG
}
value={
editingDescription
? description
: description || DESCRIPTION_PLACEHOLDER
}
renderHTML={(text) => mdParser.render(text)}
onChange={handleEditorChange}
placeholder={DESCRIPTION_PLACEHOLDER}
/>
</EditorWrapper>
{editingDescription && (
<DescriptionActions>
<Button
variant="contained"
data-testid="save-description"
onClick={handleSaveDescription}
color="primary"
size="small"
>
Save ({getMetaKey()}+⏎)
</Button>
<Button
variant="outlined"
data-testid="cancel-description"
onClick={handleCancelDescription}
ref={cancelRef}
size="small"
css={css`
margin-left: 0.5rem;
`}
>
Cancel (Esc)
</Button>
</DescriptionActions>
)}
</Description>
<CommentSection taskId={task.id} />
</Main>
<Side theme={theme}>
<TaskAssignees task={task} />
<Autocomplete
id="column-select"
size="small"
options={columns}
getOptionLabel={(option) => option.title}
renderInput={(params) => (
<TextField {...params} label="Column" variant="outlined" />
)}
value={column}
onChange={handleColumnChange}
disableClearable
openOnFocus
data-testid="edit-column"
css={css`
width: 100%;
`}
/>
<Autocomplete
id="priority-select"
size="small"
blurOnSelect
autoHighlight
options={PRIORITY_OPTIONS}
getOptionLabel={(option) => option.label}
value={PRIORITY_MAP[task.priority]}
onChange={handlePriorityChange}
renderInput={(params) => (
<TextField {...params} label="Priority" variant="outlined" />
)}
renderOption={(option) => <PriorityOption option={option} />}
openOnFocus
disableClearable
data-testid="edit-priority"
css={css`
width: 100%;
margin-top: 1rem;
`}
/>
<Autocomplete
multiple
id="labels-select"
data-testid="edit-labels"
size="small"
filterSelectedOptions
autoHighlight
openOnFocus
blurOnSelect
disableClearable
options={labels}
getOptionLabel={(option) => option.name}
value={
tasksById[taskId].labels.map(
(labelId) => labelsById[labelId]
) as Label[]
}
onChange={(_, newLabels) => handleLabelsChange(newLabels)}
renderInput={(params) => (
<TextField {...params} label="Labels" variant="outlined" />
)}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<LabelChip
key={option.id}
label={option}
size="small"
{...getTagProps({ index })}
/>
))
}
renderOption={(option) => <LabelChip label={option} size="small" />}
css={css`
width: 100%;
margin-top: 1rem;
margin-bottom: 2rem;
`}
/>
<ButtonsContainer>
<Button
startIcon={<FontAwesomeIcon fixedWidth icon={faLock} />}
onClick={handleNotImplemented}
size="small"
css={css`
font-size: 12px;
font-weight: bold;
color: ${TASK_G};
`}
>
Lock task ({getMetaKey()}+L)
</Button>
<Button
startIcon={<FontAwesomeIcon fixedWidth icon={faTrash} />}
onClick={handleDelete}
data-testid="delete-task"
size="small"
css={css`
font-size: 12px;
font-weight: bold;
color: ${TASK_G};
margin-bottom: 2rem;
`}
>
Delete task ({getMetaKey()}+⌫)
</Button>
</ButtonsContainer>
<Text>
Updated {formatDistanceToNow(new Date(task.modified))} ago
</Text>
<Text
css={css`
margin-bottom: 1rem;
`}
>
Created {formatDistanceToNow(new Date(task.created))} ago
</Text>
</Side>
</Content>
</Dialog>
);
}
Example #19
Source File: ScreenMediaItems.tsx From sync-party with GNU General Public License v3.0 | 4 votes |
export default function ScreenMediaItems({
socket
}: Props): JSX.Element | null {
const [redirect, setRedirect] = useState(false);
const [allMediaItems, setAllMediaItems] = useState<MediaItem[] | null>(
null
);
const [sortedMediaItems, setSortedMediaItems] = useState<
MediaItem[] | null
>(null);
const [allUsers, setAllUsers] = useState<{ [id: string]: string }>();
const [sorted, setSorted] = useState<{ [attribute: string]: boolean }>({});
const user = useSelector((state: RootAppState) => state.globalState.user);
const party = useSelector((state: RootAppState) => state.globalState.party);
const userItems = useSelector(
(state: RootAppState) => state.globalState.userItems
);
const dispatch = useDispatch();
const { t } = useTranslation();
useEffect(() => {
if (user) {
if (user.role === 'admin') {
const fetchAllItems = async (): Promise<void> => {
try {
const response = await Axios.get(
'/api/allMediaItems',
axiosConfig()
);
if (response.data.success === true) {
setAllMediaItems(response.data.allMediaItems);
setSortedMediaItems(response.data.allMediaItems);
} else {
dispatch(
setGlobalState({
errorMessage: t(
`apiResponseMessages.${response.data.msg}`
)
})
);
}
} catch (error) {
dispatch(
setGlobalState({
errorMessage: t(`errors.itemFetchError`)
})
);
}
};
fetchAllItems();
const fetchAllUsers = async (): Promise<void> => {
try {
const response = await Axios.get(
'/api/allUsers',
axiosConfig()
);
if (response.data.success) {
const userRegister: { [id: string]: string } = {};
response.data.allUsers.forEach((user: User) => {
userRegister[user.id] = user.username;
});
setAllUsers(userRegister);
} else {
dispatch(
setGlobalState({
errorMessage: t(
`apiResponseMessages.${response.data.msg}`
)
})
);
}
} catch (error) {
dispatch(
setGlobalState({
errorMessage: t(`errors.userFetchError`)
})
);
}
};
fetchAllUsers();
} else {
setAllMediaItems(userItems);
setSortedMediaItems(userItems);
setAllUsers({});
}
}
}, [userItems, user, dispatch, t]);
const handleDeleteButton = async (itemId: string): Promise<void> => {
if (socket) {
try {
const response = await Axios.delete(
'/api/mediaItem/' + itemId,
axiosConfig()
);
if (response.data.success) {
const updatedUserParties = await getUpdatedUserParties(
dispatch,
t
);
if (party) {
await updateCurrentParty(
dispatch,
updatedUserParties,
party
);
socket.emit('partyUpdate', { partyId: party.id });
}
await getUpdatedUserItems(dispatch, t);
socket.emit('mediaItemUpdate', {});
}
} catch (error) {
setGlobalState({
errorMessage: t('errors.deleteItemError')
});
}
}
};
const handleSort = async (
event: React.MouseEvent<Element, MouseEvent>
): Promise<void> => {
const attribute = (event.target as HTMLTableRowElement).id;
if (!sorted[attribute] && allMediaItems) {
setSortedMediaItems(
[...allMediaItems].sort(
(a: IndexableMediaItem, b: IndexableMediaItem) => {
return a[attribute] < b[attribute] ? -1 : 1;
}
)
);
setSorted({ [attribute]: true });
} else {
setSortedMediaItems(allMediaItems);
setSorted({ [attribute]: false });
}
};
if (redirect) {
return <Navigate to="/"></Navigate>;
}
return sortedMediaItems &&
sortedMediaItems.length > 0 &&
allUsers &&
user ? (
<div className="bg-black p-4">
<div className="flex flex-row justify-between">
<Heading
className="mb-5"
size={2}
text={
user.role === 'admin'
? t('mediaItems.headingAdmin')
: t('mediaItems.headingUser')
}
></Heading>
<ButtonIcon
className="p-1"
color="text-gray-200"
title={t('common.close')}
icon={
<FontAwesomeIcon
icon={faTimes}
size="lg"
></FontAwesomeIcon>
}
onClick={(): void => setRedirect(true)}
></ButtonIcon>
</div>
<div className="text-xs">
<table className="select-text w-full">
<thead>
<tr className="border-b border-gray-700 text-left">
<th
id="name"
onClick={(event): void => {
handleSort(event);
}}
className="cursor-pointer py-3 pr-3"
>
{t('mediaItems.name')}
</th>
{user.role === 'admin' && (
<>
<th
id="type"
onClick={(event): void => {
handleSort(event);
}}
className="cursor-pointer pr-3"
>
{t('mediaItems.type')}
</th>
<th
id="owner"
onClick={(event): void => {
handleSort(event);
}}
className="cursor-pointer pr-3"
>
{t('mediaItems.owner')}
</th>
</>
)}
<th
id="url"
onClick={(event): void => {
handleSort(event);
}}
className="cursor-pointer pr-3"
>
{t('mediaItems.url')}
</th>
{user.role === 'admin' && (
<th
id="id"
onClick={(event): void => {
handleSort(event);
}}
className="cursor-pointer pr-3"
>
{t('mediaItems.id')}
</th>
)}
<th
id="createdAt"
onClick={(event): void => {
handleSort(event);
}}
className="cursor-pointer pr-3"
>
{t('mediaItems.createdAt')}
</th>
{user.role === 'admin' && (
<th
id="updatedAt"
onClick={(event): void => {
handleSort(event);
}}
className="cursor-pointer pr-3"
>
{t('mediaItems.updatedAt')}
</th>
)}
<th className="pr-4">{t('mediaItems.actions')}</th>
</tr>
</thead>
<tbody>
{sortedMediaItems.map((mediaItem) => {
const createdAtRaw = new Date(mediaItem.createdAt);
const updatedAtRaw = new Date(mediaItem.updatedAt);
return (
<tr
key={mediaItem.id}
className="border-b border-gray-700"
>
<td className="pr-3 py-3">
{mediaItem.name}
</td>
{user.role === 'admin' && allUsers && (
<>
<td className="pr-3">
{mediaItem.type}
</td>
<td className="pr-3">
{allUsers[mediaItem.owner]}
</td>
</>
)}
<td className="pr-3">
{user.role === 'admin'
? mediaItem.url
: mediaItem.type === 'file'
? mediaItem.url.substr(37)
: mediaItem.url}
</td>
{user.role === 'admin' && (
<td className="pr-3">{mediaItem.id}</td>
)}
<td className="pr-3">
{createdAtRaw.toLocaleString()}
</td>
{user.role === 'admin' && (
<td className="pr-3">
{updatedAtRaw.toLocaleString()}
</td>
)}
<td className="pr-3">
<div className="flex flex-row">
<ButtonIcon
color="text-gray-200"
title={t('mediaItems.delete')}
icon={
<FontAwesomeIcon
icon={faTrash}
></FontAwesomeIcon>
}
onClick={(): Promise<void> =>
handleDeleteButton(
mediaItem.id
)
}
></ButtonIcon>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
) : null;
}
Example #20
Source File: VieraConfigUI.tsx From homebridge-vieramatic with Apache License 2.0 | 4 votes |
Body = () => {
useSingleton(() => void (async (): Promise<void> => await updateGlobalConfig())())
const state = useState(globalState)
useEffect(
() => (state.loading.value ? homebridge.showSpinner() : homebridge.hideSpinner()),
[state.loading.value]
)
const request = async (path: string, body?: unknown) => {
state.loading.set(true)
return await homebridge.request(path, body).finally(() => state.loading.set(false))
}
const previousConfig = (ip: string): UserConfig | undefined =>
state.pluginConfig.tvs.value.find((o) => o.ipAddress === ip)
const backToMain = (form?: IHomebridgeUiFormHelper) => {
if (form) form.end()
state.merge({ frontPage: true, selected: none })
}
const onEdition = async (raw?: string): Promise<void> => {
const pair = (challenge: string) => {
const pinForm = homebridge.createForm(pinRequestSchema, undefined, 'Next', 'Cancel')
pinForm.onCancel(pinForm.end)
pinForm.onSubmit(
async (data) =>
await request('/pair', { challenge, ip: tv.ipAddress, pin: data.pin })
.then(async (auth: VieraAuth) => {
const body = JSON.stringify({ ...tv, appId: auth.appId, encKey: auth.key })
const specs: VieraSpecs = await request('/specs', body)
return { auth, specs }
})
// eslint-disable-next-line promise/always-return
.then((payload) => {
const config = { ...tv, appId: payload.auth.appId, encKey: payload.auth.key }
state.selected.merge({ config, onHold: false, reachable: true, specs: payload.specs })
})
.catch(() => {
homebridge.toast.error('Wrong PIN...', tv.ipAddress)
backToMain()
})
.finally(pinForm.end)
)
}
if (!raw) {
state.frontPage.set(false)
const tvForm = homebridge.createForm(tvAddressSchema, undefined, 'Next', 'Cancel')
tvForm.onCancel(() => backToMain(tvForm))
tvForm.onSubmit(async (data) => {
if (isValidIPv4(data.ipAddress)) {
if (previousConfig(data.ipAddress))
homebridge.toast.error('Trying to add an already configured TV set!', data.ipAddress)
else {
tvForm.end()
const config = { hdmiInputs: [], ipAddress: data.ipAddress }
state.selected.merge({ config, onHold: true })
}
} else homebridge.toast.error('Please insert a valid IP address...', data.ipAddress)
})
} else
state.batch((s) => {
s.selected.merge({ config: JSON.parse(raw), onHold: true }), s.frontPage.set(false)
})
while (!state.selected.value?.config) await sleep(250)
const tv = state.selected.value.config
await request('/ping', tv.ipAddress).then(async (reachable: boolean) => {
/* eslint-disable promise/no-nesting*/
if (!reachable) return state.selected.merge({ onHold: false, reachable })
return await request('/specs', JSON.stringify(tv))
.then((specs) => state.selected.merge({ onHold: false, reachable, specs }))
.catch(async () => await request('/pin', tv.ipAddress).then((challenge) => pair(challenge)))
})
}
const onDeletion = (raw: string) =>
state.batch((s) => {
s.frontPage.set(false), s.selected.merge({ config: JSON.parse(raw), onHold: false })
})
const FrontPage = () => {
const flip = () => !state.abnormal.value && state.killSwitch.set((k) => !k)
const label = `${state.killSwitch.value ? 'deletion' : 'edition'} mode`
const doIt = (tv: string) => (state.killSwitch.value ? onDeletion(tv) : onEdition(tv))
const KillBox = () =>
state.pluginConfig.value.tvs.length === 0 ? (
<></>
) : state.abnormal.value ? (
<Alert variant="warning" className="d-flex justify-content-center mt-3 mb-5">
<b>more than one TV with same IP address found: please delete the bogus ones!</b>
</Alert>
) : (
<Form className="d-flex justify-content-end mt-3 mb-5">
<Form.Switch onChange={flip} id="kS" label={label} checked={state.killSwitch.value} />
</Form>
)
const style = { height: '4em', width: '10em' }
const AddNew = () =>
state.killSwitch.value ? (
<></>
) : (
<div className="d-flex justify-content-center mt-3 mb-5">
<Button
className="my-4"
variant="primary"
onClick={async () => await onEdition()}
style={style}
>
<Icon fixedWidth size="sm" icon={faTv} /> <br />
<Icon fixedWidth size="lg" icon={faCartPlus} />
</Button>
</div>
)
const Available = () => {
const variant = state.killSwitch.value ? 'danger' : 'info'
const onClick = (tv: UserConfig) => doIt(JSON.stringify(tv))
const tvs = state.pluginConfig.value.tvs.map((tv, index) => (
<Button variant={variant} style={style} key={index} onClick={() => onClick(tv)}>
<Icon fixedWidth size="lg" icon={state.killSwitch.value ? faTrash : faTv} />
<br /> {tv.ipAddress}
</Button>
))
return <>{tvs}</>
}
return (
<section className="mh-100">
<KillBox /> <Available /> <AddNew />
</section>
)
}
const Results = (props: { selected: State<Selected> | undefined }) => {
if (!props.selected || props.selected.onHold.value) return <></>
const Offline = (props: { selected: State<Selected> }) => (
<Alert variant="danger" className="mt-3">
<Alert.Heading>
The Viera TV at <b>{props.selected.config.ipAddress.value}</b> could not be edited.
</Alert.Heading>
<hr />
<p className="mb-2">
Please, do make sure that it is <b>turned on</b> and <b>connected to the network</b>, and
then try again.
</p>
<div className="mt-4 w-75 mx-auto">
<p className="text-left ">
Also, <b>if you haven't done it already</b>...
</p>
<p className="text-left">
...on your TV go to <b>Menu / Network / TV Remote App Settings</b> and make sure that
the following settings are <b>all</b> turned <b>ON</b>:
<ul className="mt-2">
<li>
<b>TV Remote</b>
</li>
<li>
<b>Powered On by Apps</b>
</li>
<li>
<b>Networked Standby</b>
</li>
</ul>
</p>
</div>
<div className="d-flex justify-content-end mt-5">
<Button onClick={() => backToMain()} variant="primary">
OK
</Button>
</div>
</Alert>
)
const ConfirmDeletion = (props: { selected: State<Selected> }) => {
const { ipAddress } = props.selected.config.value
const nxt = rawClone(state.pluginConfig.value.tvs.filter((o) => o.ipAddress !== ipAddress))
const dropIt = async () =>
await updateHomebridgeConfig(ipAddress, nxt, actionType.delete).then(() => backToMain())
return (
<Alert variant="danger" className="mt-3">
<Alert.Heading>
The Viera TV at <b>{ipAddress}</b> is about to be deleted from this Homebridge.
</Alert.Heading>
<hr />
<div className="d-flex justify-content-center">
<div className="w-75">
<p className="mb-2">Please, make sure you know what you are doing...</p>
<hr />
<pre class="text-monospace text-left bg-light p-2">
{prettyPrint(props.selected.config.value)}
</pre>
<hr />
</div>
</div>
<div className="d-flex justify-content-end mt-1">
<Button onClick={() => backToMain()} variant="primary">
Cancel
</Button>
<Button onClick={() => dropIt()} variant="danger">
Delete
</Button>
</div>
</Alert>
)
}
const Editor = (props: { selected: State<Selected> }) => {
if (props.selected.specs.ornull?.requiresEncryption.value)
commonFormLayout.splice(1, 0, authLayout)
const schema = { layout: commonFormLayout, schema: commonSchema }
const data = rawClone(props.selected.config.value)
const tvform = homebridge.createForm(schema, data, 'Submit', 'Cancel')
tvform.onCancel(() => backToMain(tvform))
tvform.onSubmit(async (submited) => {
const queued = submited as UserConfig
state.loading.set(true)
backToMain(tvform)
const before = previousConfig(queued.ipAddress)
let [others, type] = [[] as UserConfig[], actionType.none]
if (!isSame(before, queued)) {
const modded = before !== undefined
const { tvs } = state.pluginConfig.value
others = modded ? rawClone(tvs.filter((v) => v.ipAddress != queued.ipAddress)) : []
type = modded ? actionType.update : actionType.create
}
await updateHomebridgeConfig(queued.ipAddress, [...others, queued], type).finally(() =>
state.loading.set(false)
)
})
return <></>
}
if (state.killSwitch.value) return <ConfirmDeletion selected={props.selected} />
if (props.selected.reachable.value) return <Editor selected={props.selected} />
return <Offline selected={props.selected} />
}
return state.frontPage.value ? <FrontPage /> : <Results selected={state.selected.ornull} />
}