mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
More work on React components
This commit is contained in:
@@ -1,18 +1,12 @@
|
||||
import React, { useId, useEffect, useRef, useState } from "react"
|
||||
|
||||
import 'flowbite';
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowCircleDown, faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import { initFlowbite } from "flowbite";
|
||||
import { faArrowCircleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export function OlAccordion(props) {
|
||||
var [open, setOpen] = useState(false);
|
||||
var [scrolledUp, setScrolledUp] = useState(true);
|
||||
var [scrolledDown, setScrolledDown] = useState(false);
|
||||
|
||||
const bodyId = useId();
|
||||
const accordionId = useId();
|
||||
const headingId = useId();
|
||||
|
||||
var contentRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,20 +16,18 @@ export function OlAccordion(props) {
|
||||
setScrolledUp(e.target.scrollTop === 0);
|
||||
}
|
||||
})
|
||||
|
||||
initFlowbite();
|
||||
})
|
||||
|
||||
return <div id={accordionId} data-accordion="collapse" data-active-classes="bg-white dark:bg-transparent text-gray-900 dark:text-white" data-inactive-classes="text-gray-500 dark:text-gray-300">
|
||||
<h3 id={headingId}>
|
||||
<button type="button" className="flex items-center justify-between w-full py-2 font-medium rtl:text-right text-gray-500 border-gray-200 dark:border-gray-700 dark:text-gray-300 gap-3" data-accordion-target={"#" + CSS.escape(bodyId)} aria-expanded="false" aria-controls={bodyId}>
|
||||
return <div className="bg-white dark:bg-transparent text-gray-900 dark:text-white">
|
||||
<h3>
|
||||
<button type="button" onClick={() => setOpen(!open)} className="flex items-center justify-between w-full py-2 font-medium rtl:text-right text-gray-700 border-gray-200 dark:border-gray-700 dark:text-gray-300 gap-3">
|
||||
<span>{props.title}</span>
|
||||
<svg data-accordion-icon className="w-3 h-3 rotate-180 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<svg data-open={open} className="w-3 h-3 data-[open='false']:-scale-y-100 shrink-0 transition-transform" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5 5 1 1 5" />
|
||||
</svg>
|
||||
</button>
|
||||
</h3>
|
||||
<div id={bodyId} className="hidden" aria-labelledby={headingId}>
|
||||
<div className={open? "": "hidden"}>
|
||||
{props.showArrows && <div className="rotate-180"> {!scrolledUp && <FontAwesomeIcon icon={faArrowCircleDown} className="text-white animate-bounce opacity-20 absolute w-full"/>}</div>}
|
||||
<div ref={contentRef} className="py-2 border-gray-200 dark:border-gray-700">
|
||||
{props.children}
|
||||
|
||||
7
frontend/react/src/ui/components/olcheckbox.tsx
Normal file
7
frontend/react/src/ui/components/olcheckbox.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React, { useState, useId } from "react";
|
||||
|
||||
export function OlCheckbox(props) {
|
||||
const id = useId();
|
||||
|
||||
return <input id={id} onChange={props.onChange} type="checkbox" checked={props.checked} value="" className="w-4 h-4 my-auto text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
|
||||
}
|
||||
9
frontend/react/src/ui/components/olcoalitiontoggle.tsx
Normal file
9
frontend/react/src/ui/components/olcoalitiontoggle.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
export function OlCoalitionToggle() {
|
||||
return <div className="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" value="" className="sr-only peer" />
|
||||
<div className="relative w-14 h-7 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[4px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
<span className="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Large toggle</span>
|
||||
</div>
|
||||
}
|
||||
@@ -1,30 +1,89 @@
|
||||
|
||||
import React, { useId, useState } from "react";
|
||||
import React, { useId, useState, useEffect, useRef } from "react";
|
||||
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
library.add(faChevronDown, faChevronUp);
|
||||
|
||||
export function OlDropdown(props) {
|
||||
var [value, setValue] = useState(props.items[0] ?? "N/A" )
|
||||
export function OlTextDropdown(props) {
|
||||
var [value, setValue] = useState(props.items[0] ?? "N/A")
|
||||
const buttonId = useId();
|
||||
const dropdownId = useId()
|
||||
|
||||
|
||||
return <div>
|
||||
<button id={buttonId} data-dropdown-toggle={dropdownId} className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center border-[1px] dark:border-gray-600 dark:text-gray-400 dark:bg-gray-700 dark:hover:bg-gray-800 dark:focus:ring-blue-800" type="button"><FontAwesomeIcon icon={props.leftIcon} className="mr-3" />{value}<svg className="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div id={dropdownId} className="z-ui hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
||||
<ul className="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby={buttonId}>
|
||||
<div id={dropdownId} className="z-ui-2 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
||||
<div className="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby={buttonId}>
|
||||
{props.items.map((item) => {
|
||||
return <li>
|
||||
<a href="#" onClick={() => setValue(item)} className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">{item}</a>
|
||||
</li>
|
||||
return <OlDropdownItem onClick={() => setValue(item)}>
|
||||
{item}
|
||||
</OlDropdownItem>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function OlElementDropdown(props) {
|
||||
var [open, setOpen] = useState(false);
|
||||
var contentRef = useRef(null);
|
||||
var buttonRef = useRef(null);
|
||||
|
||||
function setPosition(content: HTMLDivElement, button: HTMLButtonElement) {
|
||||
content.style.left = `0px`;
|
||||
content.style.top = `0px`;
|
||||
|
||||
var [cxl, cyt, cxr, cyb, cw, ch] = [content.getBoundingClientRect().x, content.getBoundingClientRect().y, content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight, content.clientWidth, content.clientHeight];
|
||||
var [bxl, byt, bxr, byb, bbw, bh] = [button.getBoundingClientRect().x, button.getBoundingClientRect().y, button.getBoundingClientRect().x + button.clientWidth, button.getBoundingClientRect().y + button.clientHeight, button.clientWidth, button.clientHeight];
|
||||
|
||||
var cxc = (cxl + cxr) / 2;
|
||||
var bxc = (bxl + bxr) / 2;
|
||||
|
||||
var offsetX = bxc - cxc;
|
||||
var offsetY = byb - cyt;
|
||||
|
||||
cxl += offsetX;
|
||||
cxr += offsetX;
|
||||
|
||||
if (cxl < 0)
|
||||
offsetX -= cxl;
|
||||
if (cxr > window.innerWidth)
|
||||
offsetX -= (cxr - window.innerWidth)
|
||||
|
||||
content.style.left = `${offsetX}px`
|
||||
content.style.top = `${offsetY + 5}px`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current && buttonRef.current) {
|
||||
const content = contentRef.current as HTMLDivElement;
|
||||
const button = buttonRef.current as HTMLButtonElement;
|
||||
|
||||
setPosition(content, button);
|
||||
}
|
||||
})
|
||||
|
||||
return <div className="relative">
|
||||
<button ref={buttonRef} onClick={() => {setOpen(!open)}} className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center border-[1px] dark:border-gray-600 dark:text-gray-400 dark:bg-gray-700 dark:hover:bg-gray-800 dark:focus:ring-blue-800" type="button"><FontAwesomeIcon icon={props.leftIcon} className="mr-3" />{props.label}<svg className="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div ref={contentRef} data-open={open} className="absolute z-ui-2 w-fit data-[open='false']:hidden bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700">
|
||||
<div className="py-2 text-sm text-gray-700 dark:text-gray-200 w-fit">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function OlDropdownItem(props) {
|
||||
return <div onClick={props.onClick ?? (() => { })} className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white flex flex-row content-center gap-2">
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
11
frontend/react/src/ui/components/ollabeltoggle.tsx
Normal file
11
frontend/react/src/ui/components/ollabeltoggle.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
export function OlLabelToggle(props) {
|
||||
var [toggled, setToggled] = useState(false);
|
||||
|
||||
return <div onClick={() => {setToggled(!toggled)}} className="relative flex flex-row contents-center justify-between w-32 h-10 dark:bg-gray-700 rounded-md py-1 px-1 select-none cursor-pointer">
|
||||
<span data-toggled={toggled} className="absolute my-auto h-8 w-14 bg-blue-500 rounded-md data-[toggled='true']:translate-x-16 transition-transform"></span>
|
||||
<span data-active={!toggled} className="my-auto dark:data-[active='true']:text-white dark:data-[active='false']:text-gray-400 text-thin pl-3 z-ui-2 transition-colors">MSL</span>
|
||||
<span data-active={toggled} className="my-auto dark:data-[active='true']:text-white dark:data-[active='false']:text-gray-400 text-thin pr-3 z-ui-2 transition-colors">AGL</span>
|
||||
</div>
|
||||
}
|
||||
19
frontend/react/src/ui/components/olnumberinput.tsx
Normal file
19
frontend/react/src/ui/components/olnumberinput.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, {useEffect, useId} from "react";
|
||||
|
||||
export function OlNumberInput(props) {
|
||||
return <div className="w-fit">
|
||||
<div className="relative flex items-center max-w-[8rem]">
|
||||
<button type="button" className="bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:border-gray-600 hover:bg-gray-200 border border-gray-300 rounded-s-lg p-3 h-9 focus:ring-gray-100 dark:focus:ring-gray-700 focus:ring-2 focus:outline-none">
|
||||
<svg className="w-3 h-3 text-gray-900 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 2">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 1h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="text" className="bg-gray-50 border-x-0 border-gray-300 h-9 text-center text-gray-900 text-sm focus:ring-blue-500 focus:border-blue-500 block w-full py-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder={props.placeHolder} />
|
||||
<button type="button" className="bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:border-gray-600 hover:bg-gray-200 border border-gray-300 rounded-e-lg p-3 h-9 focus:ring-gray-100 dark:focus:ring-gray-700 focus:ring-2 focus:outline-none">
|
||||
<svg className="w-3 h-3 text-gray-900 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 18">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 1v16M1 9h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
11
frontend/react/src/ui/components/olrangeslider.tsx
Normal file
11
frontend/react/src/ui/components/olrangeslider.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
export function OlRangeSlider(props) {
|
||||
return <input type="range"
|
||||
onChange={(ev) => { props.onChange(Number(ev.target?.value ?? props.value)) }}
|
||||
value={props.value}
|
||||
min={props.minValue ?? 0}
|
||||
max={props.maxValue ?? 100}
|
||||
step={props.step ?? 1}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" />
|
||||
}
|
||||
@@ -2,10 +2,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import React from "react"
|
||||
|
||||
export function OlStateButton(props) {
|
||||
return <button {...props} type="button" className={`h-[40px] w-[40px] m-auto text-gray-900 bg-white border border-gray-300 focus:outline-none
|
||||
hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm
|
||||
dark:bg-${props.checked? 'white': 'transparent'} dark:text-${props.checked? 'gray-900': 'white'} dark:border-gray-600
|
||||
dark:hover:bg-${props.checked? 'white': 'gray-700'} dark:hover:border-gray-600 dark:focus:ring-gray-700`}>
|
||||
const className = (props.className ?? '') + ` h-[40px] w-[40px] m-auto border border-gray-900 font-medium rounded-md text-sm ` +
|
||||
`dark:bg-transparent dark:data-[checked='true']:bg-white dark:text-white dark:data-[checked='true']:text-gray-900 dark:border-gray-600 `;
|
||||
|
||||
return <button onClick={props.onClick} data-checked={props.checked} type="button" className={className}>
|
||||
<FontAwesomeIcon icon={props.icon} />
|
||||
</button>
|
||||
}
|
||||
|
||||
export function OlRoundStateButton(props) {
|
||||
const className = (props.className ?? '') + ` h-[40px] w-[40px] m-auto border border-gray-900 font-medium rounded-full text-sm ` +
|
||||
`dark:bg-transparent dark:data-[checked='true']:bg-white dark:text-white dark:data-[checked='true']:text-gray-900 dark:border-gray-600 `;
|
||||
|
||||
return <button onClick={props.onClick} data-checked={props.checked} type="button" className={className}>
|
||||
<FontAwesomeIcon icon={props.icon} />
|
||||
</button>
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import React, { ChangeEvent } from "react";
|
||||
|
||||
type OlToggleState = {
|
||||
checked: boolean
|
||||
}
|
||||
|
||||
type OlToggleProps = {
|
||||
checkedLabel: string,
|
||||
uncheckedLabel: string
|
||||
}
|
||||
|
||||
export class OlToggle extends React.Component<OlToggleProps, OlToggleState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
checked: false
|
||||
}
|
||||
|
||||
this.onToggle = this.onToggle.bind(this);
|
||||
}
|
||||
|
||||
onToggle(e: ChangeEvent<HTMLInputElement>) {
|
||||
this.setState({
|
||||
checked: e.target.checked
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <label className="flex items-center cursor-pointer -mr-8">
|
||||
<input type="checkbox" value="" className="sr-only peer" onChange={this.onToggle}/>
|
||||
<div className="relative w-16 h-6 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-500 peer-checked:after:translate-x-[200%] rtl:peer-checked:after:-translate-x-[200%] peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-interaction-blue"></div>
|
||||
<span className={(this.state.checked? "-translate-x-[250%]": "-translate-x-[180%]") + " ms-3 text-sm font-medium text-gray-900 dark:text-white transition-all select-none relative w-7"}>
|
||||
{this.state.checked? this.props.checkedLabel: this.props.uncheckedLabel}
|
||||
</span>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import React, { useId, useState, useRef } from "react";
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import 'flowbite';
|
||||
|
||||
export function OlUnitEntryList(props) {
|
||||
return <div {...props} className="relative text-sm cursor-pointer select-none flex justify-between items-center dark:text-gray-300 dark:hover:bg-white dark:hover:bg-opacity-10 px-2 py-1 rounded-sm mr-2">
|
||||
|
||||
@@ -2,8 +2,7 @@ import React from "react";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
|
||||
export function OlUnitSummary(props: {blueprint: UnitBlueprint}) {
|
||||
console.log(props.blueprint)
|
||||
return <div {...props} className="relative border-l-4 border-blue-600 flex flex-col gap-2 p-2 items-start shadow-lg bg-white rounded-md hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-700">
|
||||
return <div {...props} className="relative border-l-4 border-blue-600 flex flex-col gap-2 p-2 pt-4 items-start shadow-lg bg-white rounded-md hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-700">
|
||||
<div className="flex flex-row gap-2 content-center">
|
||||
<img className="object-cover h-8 ml-2 rounded-tl-md rotate-180 invert" src={"images/units/"+props.blueprint.filename} alt="" />
|
||||
<div className="my-auto w-full font-bold tracking-tight text-gray-900 dark:text-white">{props.blueprint.label}</div>
|
||||
@@ -13,7 +12,7 @@ export function OlUnitSummary(props: {blueprint: UnitBlueprint}) {
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 p-2">
|
||||
{props.blueprint.abilities?.split(" ").map((tag) => {
|
||||
return <div className="bg-black bg-opacity-20 dark:text-gray-400 rounded-full px-2 py-0.5 text-xs">
|
||||
return <div key={tag} className="bg-black bg-opacity-20 dark:text-gray-400 rounded-full px-2 py-0.5 text-xs">
|
||||
{tag}
|
||||
</div>
|
||||
})}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from "react";
|
||||
import { UnitBlueprint } from "../../../interfaces";
|
||||
import { IconProp, library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
library.add(faChevronLeft);
|
||||
|
||||
export type BlueprintsAccordionProps = {
|
||||
title: string,
|
||||
icon: string,
|
||||
blueprints: { [key: string]: UnitBlueprint },
|
||||
searchString: string,
|
||||
callback: CallableFunction
|
||||
}
|
||||
|
||||
export type BlueprintsAccordionState = {
|
||||
open: boolean
|
||||
}
|
||||
|
||||
export class BlueprintsAccordion extends React.Component<BlueprintsAccordionProps, BlueprintsAccordionState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: this.props.searchString !== ''
|
||||
}
|
||||
|
||||
this.toggleOpen = this.toggleOpen.bind(this);
|
||||
}
|
||||
|
||||
toggleOpen() {
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
|
||||
checkSearch(key) {
|
||||
const blueprint = this.props.blueprints[key];
|
||||
if (blueprint.label.includes(this.props.searchString))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.searchString !== '' && !this.state.open)
|
||||
this.setState({ open: true });
|
||||
|
||||
return <div>
|
||||
<div className="cursor-pointer select-none flex justify-between p-2 items-center" onClick={this.toggleOpen}>
|
||||
<div className="">{this.props.title}</div>
|
||||
<FontAwesomeIcon icon="chevron-left" className={"transition-transform " + (this.state.open ? "-rotate-90" : "")}></FontAwesomeIcon>
|
||||
</div>
|
||||
<div className="flex flex-col px-3">
|
||||
{
|
||||
this.state.open &&
|
||||
Object.keys(this.props.blueprints).filter((key) => {
|
||||
return this.checkSearch(key);
|
||||
}).map((key) => {
|
||||
return <div key={key} className="cursor-pointer select-none flex justify-between items-center hover:bg-white hover:bg-opacity-10 px-2 py-1 rounded-sm" onClick={() => this.props.callback(this.props.blueprints[key])}>
|
||||
<FontAwesomeIcon icon={this.props.icon as IconProp} className="text-sm"></FontAwesomeIcon>
|
||||
<div className="font-normal text-left flex-1 px-2">{this.props.blueprints[key].label}</div>
|
||||
<div className="bg-black bg-opacity-20 text-gray-400 rounded-full px-2 py-0.5 text-xs">{this.props.blueprints[key].era === "WW2" ? "WW2" : this.props.blueprints[key].era.split(" ").map((word) => {
|
||||
return word.charAt(0).toLocaleUpperCase();
|
||||
})}</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
import { Drawer, DrawerInterface } from "flowbite";
|
||||
import React, { useEffect, useId, useRef } from "react";
|
||||
import React from "react";
|
||||
|
||||
export function Menu(props) {
|
||||
const ref = useRef(null);
|
||||
const labelId = useId();
|
||||
|
||||
useEffect(() => {
|
||||
const drawer: DrawerInterface = new Drawer(ref.current, { backdrop: false });
|
||||
props.open ? drawer.show() : drawer.hide();
|
||||
})
|
||||
|
||||
return <div ref={ref} className="w-[430px] absolute top-[80px] left-0 z-ui h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white dark:bg-gray-800" tabIndex={-1} aria-labelledby={labelId}>
|
||||
<h5 id={labelId} className="w-full inline-flex items-center pb-3 mb-4 border-b-2 dark:border-gray-700 text-base font-semibold text-gray-500 dark:text-gray-400">
|
||||
return <div data-open={props.open} className="w-[430px] absolute top-[80px] left-20 z-ui-0 h-screen p-4 overflow-y-auto transition-transform data-[open='false']:-translate-x-full bg-gray-200 dark:bg-gray-800" tabIndex={-1}>
|
||||
<h5 className="w-full inline-flex items-center pb-3 mb-4 border-b-2 dark:border-gray-700 text-base font-semibold text-gray-900 dark:text-gray-400">
|
||||
{props.title}
|
||||
</h5>
|
||||
<button type="button" onClick={props.closeCallback} className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 absolute top-2.5 end-2.5 flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white" >
|
||||
<button type="button" onClick={props.closeCallback} className="text-gray-900 dark:text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 absolute top-2.5 end-2.5 flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white" >
|
||||
<svg className="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react'
|
||||
import { OlStateButton } from '../components/olstatebutton';
|
||||
import { faPlus, faGamepad, faRuler, faPencil, faMap } from '@fortawesome/free-solid-svg-icons';
|
||||
import React, { useEffect } from 'react'
|
||||
import { OlRoundStateButton } from '../components/olstatebutton';
|
||||
import { faPlus, faGamepad, faRuler, faPencil, faMap, faLock, faPerson, faBrain, faRobot, faJetFighter, faHelicopter, faShield, faTruck, faShip, faPlaneDeparture, faSkull, faShieldAlt, faGears } from '@fortawesome/free-solid-svg-icons';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { EventsConsumer } from '../../eventscontext';
|
||||
import { StateConsumer } from '../../statecontext';
|
||||
import { OlDropdown } from '../components/oldropdown';
|
||||
import { OlDropdownItem, OlElementDropdown, OlTextDropdown } from '../components/oldropdown';
|
||||
import { OlCheckbox } from '../components/olcheckbox';
|
||||
import { MAP_OPTIONS_DEFAULTS, MAP_OPTIONS_TOOLTIPS } from '../../constants/constants';
|
||||
import { getApp } from '../../olympusapp';
|
||||
|
||||
library.add(faPlus, faGamepad, faRuler, faPencil, faMap);
|
||||
|
||||
@@ -13,16 +16,72 @@ export function Header(props) {
|
||||
{(appState) =>
|
||||
<EventsConsumer>
|
||||
{(events) =>
|
||||
<nav className="bg-white border-gray-200 dark:bg-[#171C26] dark:border-gray-700">
|
||||
<div className="max-w-screen flex flex-wrap items-center justify-between p-4">
|
||||
<nav className="bg-gray-300 border-gray-200 dark:bg-[#171C26] dark:border-gray-700">
|
||||
<div className="max-w-screen flex flex-wrap items-center justify-between p-4 gap-3">
|
||||
<div className="flex flex-row items-center justify-center gap-1">
|
||||
<img src="images/icon.png" className='h-12 border-2 p-0 bg-white rounded-md mr-2 cursor-pointer' onClick={events.toggleMainMenuVisible}></img>
|
||||
<OlStateButton onClick={events.toggleSpawnMenuVisible} checked={appState.spawnMenuVisible} icon="fa-solid fa-plus"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleUnitControlMenuVisible} checked={appState.unitControlMenuVisible} icon="fa-solid fa-gamepad"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleMeasureMenuVisible} checked={appState.measureMenuVisible} icon="fa-solid fa-ruler"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleDrawingMenuVisible} checked={appState.drawingMenuVisible} icon="fa-solid fa-pencil"></OlStateButton>
|
||||
<img src="images/icon.png" className='h-12 p-0 rounded-md mr-2 cursor-pointer' onClick={events.toggleMainMenuVisible}></img>
|
||||
</div>
|
||||
<OlDropdown items={["DCS Sat", "DCS Alt"]} leftIcon='fa-solid fa-map' />
|
||||
<div className="ml-auto">
|
||||
<OlRoundStateButton icon={faLock} />
|
||||
</div>
|
||||
<div className="flex flex-row h-fit items-center justify-start gap-1">
|
||||
{
|
||||
Object.entries({
|
||||
'human': faPerson,'olympus': faBrain, 'dcs': faRobot
|
||||
}).map((entry) => {
|
||||
return <OlRoundStateButton
|
||||
onClick={() => {
|
||||
getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]);
|
||||
}}
|
||||
checked={!appState.mapHiddenTypes[entry[0]]}
|
||||
icon={entry[1]} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className='h-10 w-0 border-l-2 border-gray-500'></div>
|
||||
<div className="flex flex-row h-fit items-center justify-start gap-1">
|
||||
{
|
||||
Object.entries({
|
||||
'aircraft': faJetFighter,'helicopter': faHelicopter, 'groundunit-sam': faShieldAlt,
|
||||
'groundunit': faTruck, 'navyunit': faShip, 'airbase': faPlaneDeparture, 'dead': faSkull
|
||||
}).map((entry) => {
|
||||
return <OlRoundStateButton
|
||||
onClick={() => {
|
||||
getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]);
|
||||
}}
|
||||
checked={!appState.mapHiddenTypes[entry[0]]}
|
||||
icon={entry[1]} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className='h-10 w-0 border-l-2 border-gray-500'></div>
|
||||
<div className="flex flex-row h-fit items-center justify-start gap-1">
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType( 'blue', !appState.mapHiddenTypes['blue'] )}
|
||||
checked={!appState.mapHiddenTypes['blue']}
|
||||
icon={faShield} className={"!text-blue-500"} />
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType('red', !appState.mapHiddenTypes['red'] )}
|
||||
checked={!appState.mapHiddenTypes['red']}
|
||||
icon={faShield} className={"!text-red-500"} />
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType('neutral', !appState.mapHiddenTypes['neutral'] )}
|
||||
checked={!appState.mapHiddenTypes['neutral']}
|
||||
icon={faShield} className={"!text-gray-500"} />
|
||||
</div>
|
||||
<OlTextDropdown items={["DCS Sat", "DCS Alt"]} leftIcon='fa-solid fa-map' />
|
||||
<OlElementDropdown leftIcon={faGears} label="Options" className="w-80">
|
||||
{Object.keys(MAP_OPTIONS_TOOLTIPS).map((key) => {
|
||||
return <OlDropdownItem>
|
||||
<OlCheckbox
|
||||
checked = {appState.mapOptions[key]}
|
||||
onChange = {(ev) => {
|
||||
getApp().getMap()?.setOption(key, ev.target.checked);
|
||||
}}/>
|
||||
<span className="text-nowrap">{ MAP_OPTIONS_TOOLTIPS[key] }</span>
|
||||
</OlDropdownItem>
|
||||
})}
|
||||
</OlElementDropdown>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
|
||||
export function MainMenu(props) {
|
||||
return <Menu {...props} title="DCS Olympus">
|
||||
<div className="flex flex-col gap-2 text-md font-normal font text-white">
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center text-[#8BFF63]"><FontAwesomeIcon icon={faCheckCircle} className="my-auto" />Version {VERSION}</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faGithub} className="my-auto text-gray-400" />Overview</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileAlt} className="my-auto text-gray-400" />User guide</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faDatabase} className="my-auto text-gray-400" />Database Manager</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileExport} className="my-auto text-gray-400" />Export to file</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileImport} className="my-auto text-gray-400" />Import from file</div>
|
||||
<div className="flex flex-col gap-2 text-md font-normal font text-gray-900 dark:text-white">
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center text-green-700 dark:text-[#8BFF63]"><FontAwesomeIcon icon={faCheckCircle} className="my-auto" />Version {VERSION}</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faGithub} className="my-auto text-gray-800 dark:text-gray-400" />Overview</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileAlt} className="my-auto text-gray-800 dark:text-gray-400" />User guide</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faDatabase} className="my-auto text-gray-800 dark:text-gray-400" />Database Manager</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileExport} className="my-auto text-gray-800 dark:text-gray-400" />Export to file</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileImport} className="my-auto text-gray-800 dark:text-gray-400" />Import from file</div>
|
||||
</div>
|
||||
</Menu>
|
||||
}
|
||||
29
frontend/react/src/ui/panels/sidebar.tsx
Normal file
29
frontend/react/src/ui/panels/sidebar.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import { OlStateButton } from '../components/olstatebutton';
|
||||
import { faPlus, faGamepad, faRuler, faPencil, faMap } from '@fortawesome/free-solid-svg-icons';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { EventsConsumer } from '../../eventscontext';
|
||||
import { StateConsumer } from '../../statecontext';
|
||||
|
||||
library.add(faPlus, faGamepad, faRuler, faPencil, faMap);
|
||||
|
||||
export function SideBar(props) {
|
||||
return <StateConsumer>
|
||||
{(appState) =>
|
||||
<EventsConsumer>
|
||||
{(events) =>
|
||||
<nav className="z-ui-1 h-full bg-gray-300 border-gray-200 dark:bg-[#171C26] dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-center p-4 w-20">
|
||||
<div className="flex flex-col items-center justify-center gap-1">
|
||||
<OlStateButton onClick={events.toggleSpawnMenuVisible} checked={appState.spawnMenuVisible} icon="fa-solid fa-plus"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleUnitControlMenuVisible} checked={appState.unitControlMenuVisible} icon="fa-solid fa-gamepad"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleMeasureMenuVisible} checked={appState.measureMenuVisible} icon="fa-solid fa-ruler"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleDrawingMenuVisible} checked={appState.drawingMenuVisible} icon="fa-solid fa-pencil"></OlStateButton>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
</EventsConsumer>
|
||||
}
|
||||
</StateConsumer>
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlUnitEntryList } from "../components/olunitlistentry";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { UnitSpawnMenu } from "./unitspawnmenu";
|
||||
|
||||
library.add(faPlus);
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
import React from "react";
|
||||
import React, {useState} from "react";
|
||||
import { OlUnitSummary } from "../components/olunitsummary";
|
||||
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { OlLabelToggle } from "../components/ollabeltoggle";
|
||||
import { OlRangeSlider } from "../components/olrangeslider";
|
||||
|
||||
export function UnitSpawnMenu(props) {
|
||||
return <div>
|
||||
var [spawnAltitude, setSpawnAltitude] = useState(1000);
|
||||
|
||||
return <div className="flex flex-col gap-3">
|
||||
<OlUnitSummary blueprint={props.blueprint}/>
|
||||
<div className="flex flex-row content-center justify-between w-full">
|
||||
<OlCoalitionToggle />
|
||||
<OlNumberInput placeHolder={1} minValue={1} maxValue={4}/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-row content-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="dark: text-white">Altitude</span>
|
||||
<span className="dark:text-blue-500">{`${spawnAltitude} FT`}</span>
|
||||
</div>
|
||||
<OlLabelToggle />
|
||||
</div>
|
||||
<OlRangeSlider onChange={setSpawnAltitude} minValue={0} maxValue={30000} step={500}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
29
frontend/react/src/ui/ui.css
Normal file
29
frontend/react/src/ui/ui.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #FFFFFFAA;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
91
frontend/react/src/ui/ui.tsx
Normal file
91
frontend/react/src/ui/ui.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import './ui.css'
|
||||
|
||||
import { EventsProvider } from '../eventscontext'
|
||||
import { StateProvider } from '../statecontext'
|
||||
|
||||
import { Header } from './panels/header'
|
||||
import { SpawnMenu } from './panels/spawnmenu'
|
||||
import { UnitControlMenu } from './panels/unitcontrolmenu'
|
||||
import { MainMenu } from './panels/mainmenu'
|
||||
import { SideBar } from './panels/sidebar';
|
||||
import { MapHiddenTypes, MapOptions } from '../types/types'
|
||||
import { MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from '../constants/constants'
|
||||
import { getApp } from '../olympusapp'
|
||||
import { Dropdown } from 'flowbite'
|
||||
|
||||
export type OlympusState = {
|
||||
mainMenuVisible: boolean,
|
||||
spawnMenuVisible: boolean,
|
||||
unitControlMenuVisible: boolean,
|
||||
measureMenuVisible: boolean,
|
||||
drawingMenuVisible: boolean,
|
||||
mapHiddenTypes: MapHiddenTypes;
|
||||
mapOptions: MapOptions;
|
||||
}
|
||||
|
||||
export function UI(props) {
|
||||
var [mainMenuVisible, setMainMenuVisible] = useState(false);
|
||||
var [spawnMenuVisible, setSpawnMenuVisible] = useState(false);
|
||||
var [unitControlMenuVisible, setUnitControlMenuVisible] = useState(false);
|
||||
var [measureMenuVisible, setMeasureMenuVisible] = useState(false);
|
||||
var [drawingMenuVisible, setDrawingMenuVisible] = useState(false);
|
||||
var [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
||||
var [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
|
||||
|
||||
document.addEventListener("hiddenTypesChanged", (ev) => {
|
||||
setMapHiddenTypes({...getApp().getMap().getHiddenTypes()});
|
||||
})
|
||||
|
||||
document.addEventListener("mapOptionsChanged", (ev) => {
|
||||
setMapOptions({...getApp().getMap().getOptions()});
|
||||
})
|
||||
|
||||
function hideAllMenus() {
|
||||
setMainMenuVisible(false);
|
||||
setSpawnMenuVisible(false);
|
||||
setUnitControlMenuVisible(false);
|
||||
setMeasureMenuVisible(false);
|
||||
setDrawingMenuVisible(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 h-screen w-screen font-sans overflow-hidden">
|
||||
<StateProvider value={{
|
||||
mainMenuVisible: mainMenuVisible,
|
||||
spawnMenuVisible: spawnMenuVisible,
|
||||
unitControlMenuVisible: unitControlMenuVisible,
|
||||
measureMenuVisible: measureMenuVisible,
|
||||
drawingMenuVisible: drawingMenuVisible,
|
||||
mapOptions: mapOptions,
|
||||
mapHiddenTypes: mapHiddenTypes
|
||||
}}>
|
||||
<EventsProvider value={
|
||||
{
|
||||
setMainMenuVisible: setMainMenuVisible,
|
||||
setSpawnMenuVisible: setSpawnMenuVisible,
|
||||
setUnitControlMenuVisible: setUnitControlMenuVisible,
|
||||
setDrawingMenuVisible: setDrawingMenuVisible,
|
||||
setMeasureMenuVisible: setMeasureMenuVisible,
|
||||
toggleMainMenuVisible: () => { hideAllMenus(); setMainMenuVisible(!mainMenuVisible) },
|
||||
toggleSpawnMenuVisible: () => { hideAllMenus(); setSpawnMenuVisible(!spawnMenuVisible) },
|
||||
toggleUnitControlMenuVisible: () => { hideAllMenus(); setUnitControlMenuVisible(!unitControlMenuVisible) },
|
||||
toggleMeasureMenuVisible: () => { hideAllMenus(); setMeasureMenuVisible(!measureMenuVisible) },
|
||||
toggleDrawingMenuVisible: () => { hideAllMenus(); setDrawingMenuVisible(!drawingMenuVisible) },
|
||||
}
|
||||
}>
|
||||
<div className='absolute top-0 left-0 h-full w-full flex flex-col'>
|
||||
<Header />
|
||||
<div className='flex h-full'>
|
||||
<SideBar />
|
||||
<div id='map-container' className='relative h-full w-screen top-0 left-0' />
|
||||
<MainMenu open={mainMenuVisible} closeCallback={() => setMainMenuVisible(false)} />
|
||||
<SpawnMenu open={spawnMenuVisible} closeCallback={() => setSpawnMenuVisible(false)} />
|
||||
<UnitControlMenu open={unitControlMenuVisible} closeCallback={() => setUnitControlMenuVisible(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</EventsProvider>
|
||||
</StateProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user