From a7f01730eba548cfd6542f1da6c04b37d9f7063e Mon Sep 17 00:00:00 2001 From: srgooglo Date: Mon, 3 Jan 2022 18:36:51 +0100 Subject: [PATCH] refactor App with evite scattfold --- packages/app/src/App.jsx | 174 +++---- .../app/src/components/AboutApp/index.jsx | 16 +- .../app/src/components/ActionsBar/index.jsx | 4 +- .../app/src/components/ActionsBar/index.less | 53 +- packages/app/src/components/Clock/index.jsx | 17 + .../operations => Clock}/index.less | 0 .../FabricCreator/components/index.js | 1 - .../components/operations/index.jsx | 37 -- .../src/components/FabricCreator/index.jsx | 480 ------------------ .../src/components/FabricCreator/index.less | 122 ----- .../app/src/components/QRReader/index.jsx | 52 -- .../components/ScheduledProgress/index.jsx | 85 ++++ .../components/ScheduledProgress/index.less | 68 +++ .../src/components/SelectableList/index.jsx | 272 +++++----- .../src/components/SelectableList/index.less | 95 +++- .../app/src/components/Settings/index.jsx | 15 +- .../app/src/components/Settings/index.less | 8 +- .../app/src/components/UserSelector/index.jsx | 120 +++++ .../src/components/UserSelector/index.less | 19 + packages/app/src/components/index.js | 5 +- packages/app/src/controllers/index.js | 2 - packages/app/src/extensions/render/index.jsx | 152 +++--- packages/app/src/extensions/sound/index.js | 9 +- packages/app/src/i18n/index.js | 57 +++ packages/app/src/i18n/locales.js | 18 + packages/app/src/i18n/translations/en.json | 88 ++++ packages/app/src/layout/bottombar/index.jsx | 68 +++ packages/app/src/layout/bottombar/index.less | 53 ++ packages/app/src/layout/drawer/index.jsx | 204 ++++---- packages/app/src/layout/drawer/index.less | 66 ++- packages/app/src/layout/header/index.jsx | 32 +- packages/app/src/layout/index.js | 11 - packages/app/src/layout/index.jsx | 104 ++++ .../sidebar/components/editor/index.jsx | 2 +- .../sidebar/components/selector/index.jsx | 2 +- packages/app/src/layout/sidebar/index.jsx | 4 +- packages/app/src/layout/sidebar/index.less | 5 +- packages/app/src/locales/en.js | 3 - packages/app/src/locales/es.js | 12 - packages/app/src/models/session/index.js | 135 +++++ .../{controllers => models}/settings/index.js | 0 .../{controllers => models}/sidebar/index.js | 0 packages/app/src/models/user/index.js | 10 + .../account/components/sessionsView/index.jsx | 9 +- packages/app/src/pages/account/index.jsx | 18 +- packages/app/src/pages/explore/index.js | 8 - packages/app/src/pages/explore/index.less | 3 - packages/app/src/pages/main/index.jsx | 45 +- packages/app/src/pages/main/index.less | 72 ++- packages/app/src/pages/streams/index.jsx | 54 ++ .../app/src/pages/streams/viewer/index.jsx | 99 ++++ packages/app/src/pages/users/index.jsx | 93 ++++ packages/app/src/pages/users/index.less | 52 ++ packages/app/src/theme/index.less | 73 ++- packages/app/src/theme/vars.less | 3 +- 55 files changed, 1941 insertions(+), 1268 deletions(-) create mode 100644 packages/app/src/components/Clock/index.jsx rename packages/app/src/components/{FabricCreator/components/operations => Clock}/index.less (100%) delete mode 100644 packages/app/src/components/FabricCreator/components/index.js delete mode 100644 packages/app/src/components/FabricCreator/components/operations/index.jsx delete mode 100644 packages/app/src/components/FabricCreator/index.jsx delete mode 100644 packages/app/src/components/FabricCreator/index.less delete mode 100644 packages/app/src/components/QRReader/index.jsx create mode 100644 packages/app/src/components/ScheduledProgress/index.jsx create mode 100644 packages/app/src/components/ScheduledProgress/index.less create mode 100644 packages/app/src/components/UserSelector/index.jsx create mode 100644 packages/app/src/components/UserSelector/index.less delete mode 100644 packages/app/src/controllers/index.js create mode 100644 packages/app/src/i18n/index.js create mode 100644 packages/app/src/i18n/locales.js create mode 100644 packages/app/src/i18n/translations/en.json create mode 100644 packages/app/src/layout/bottombar/index.jsx create mode 100644 packages/app/src/layout/bottombar/index.less delete mode 100644 packages/app/src/layout/index.js create mode 100644 packages/app/src/layout/index.jsx delete mode 100644 packages/app/src/locales/en.js delete mode 100644 packages/app/src/locales/es.js create mode 100644 packages/app/src/models/session/index.js rename packages/app/src/{controllers => models}/settings/index.js (100%) rename packages/app/src/{controllers => models}/sidebar/index.js (100%) delete mode 100644 packages/app/src/pages/explore/index.js delete mode 100644 packages/app/src/pages/explore/index.less create mode 100644 packages/app/src/pages/streams/index.jsx create mode 100644 packages/app/src/pages/streams/viewer/index.jsx create mode 100644 packages/app/src/pages/users/index.jsx create mode 100644 packages/app/src/pages/users/index.less diff --git a/packages/app/src/App.jsx b/packages/app/src/App.jsx index 6e8fa47f..5fd1f183 100644 --- a/packages/app/src/App.jsx +++ b/packages/app/src/App.jsx @@ -11,19 +11,16 @@ String.prototype.toTitleCase = function () { } import React from "react" -import { CreateEviteApp, BindPropsProvider } from "evite" +import { CreateEviteApp, BindPropsProvider } from "evite-react-lib" import { Helmet } from "react-helmet" import * as antd from "antd" -import progressBar from "nprogress" -import classnames from "classnames" -import { SidebarController, SettingsController } from "controllers" -import { Session, User } from "models" +import { Session, User, SidebarController, SettingsController } from "models" import { API, Render, Splash, Theme, Sound } from "extensions" import config from "config" -import { NotFound, RenderError, FabricCreator, Settings } from "components" -import { Sidebar, Header, Drawer, Sidedrawer } from "./layout" +import { NotFound, RenderError, Settings } from "components" +import Layout from "./layout" import { Icons } from "components/Icons" import "theme/index.less" @@ -56,13 +53,31 @@ class ThrowCrash { } } +const __renderTest = () => { + const [position, setPosition] = React.useState(0) + + // create a 300ms interval to move randomly inside window screen + React.useEffect(() => { + setInterval(() => { + const x = Math.random() * window.innerWidth + const y = Math.random() * window.innerHeight + + setPosition({ x, y }) + }, 50) + }, []) + + // clear interval when component unmount + React.useEffect(() => { + return () => { + clearInterval() + } + }, []) + + return
+} + class App { static initialize() { - this.progressBar = progressBar.configure({ parent: "html", showSpinner: false }) - - this.sessionController = new Session() - this.userController = new User() - this.configuration = { settings: new SettingsController(), sidebar: new SidebarController(), @@ -70,45 +85,41 @@ class App { this.eventBus = this.contexts.main.eventBus - this.eventBus.on("app_ready", () => { - this.setState({ initialized: true }) - }) - this.eventBus.on("top_loadBar_start", () => { - this.progressBar.start() - }) - this.eventBus.on("top_loadBar_stop", () => { - this.progressBar.done() + this.eventBus.on("app_loading", async () => { + await this.setState({ initialized: false }) + this.eventBus.emit("splash_show") }) - this.eventBus.on("forceInitialize", async () => { - await this.initialization() + this.eventBus.on("app_ready", async () => { + await this.setState({ initialized: true }) + this.eventBus.emit("splash_close") }) - this.eventBus.on("forceReloadUser", async () => { - await this.__init_user() + + this.eventBus.on("reinitializeSession", async () => { + await this.__SessionInit() }) - this.eventBus.on("forceReloadSession", async () => { - await this.__init_session() + this.eventBus.on("reinitializeUser", async () => { + await this.__UserInit() }) + this.eventBus.on("forceToLogin", () => { if (window.location.pathname !== "/login") { this.beforeLoginLocation = window.location.pathname } + window.app.setLocation("/login") }) - this.eventBus.on("destroyAllSessions", async () => { - await this.sessionController.destroyAllSessions() - }) - this.eventBus.on("new_session", () => { - this.eventBus.emit("forceInitialize") + this.eventBus.on("new_session", async () => { + await this.initialization() if (window.location.pathname == "/login") { window.app.setLocation(this.beforeLoginLocation ?? "/main") this.beforeLoginLocation = null } }) - this.eventBus.on("destroyed_session", () => { - this.flushState() + this.eventBus.on("destroyed_session", async () => { + await this.flushState() this.eventBus.emit("forceToLogin") }) @@ -126,19 +137,13 @@ class App { } }) - this.eventBus.on("setLocation", () => { - this.eventBus.emit("top_loadBar_start") - this.setState({ isOnTransition: true }) - }) - this.eventBus.on("setLocationDone", () => { - this.eventBus.emit("top_loadBar_stop") - this.setState({ isOnTransition: false }) - }) this.eventBus.on("cleanAll", () => { window.app.DrawerController.closeAll() }) this.eventBus.on("crash", (message, error) => { + console.debug("[App] crash detecting, returning crash...") + this.setState({ crash: { message, error } }) this.contexts.app.SoundEngine.play("crash") }) @@ -149,25 +154,20 @@ class App { openSettings: (goTo) => { window.app.DrawerController.open("settings", Settings, { props: { - width: "40%", + width: "fit-content", }, componentProps: { goTo, } }) }, - openFabric: (defaultType) => { - window.app.DrawerController.open("FabricCreator", FabricCreator, { - props: { - width: "70%", - }, - componentProps: { - defaultType, - } - }) + goMain: () => { + return window.app.setLocation(config.app.mainPath) + }, + goToAccount: (username) => { + return window.app.setLocation(`/account`, { username }) }, configuration: this.configuration, - isValidSession: this.isValidSession, getSettings: (...args) => this.contexts.app.configuration?.settings?.get(...args), } } @@ -178,7 +178,6 @@ class App { sessionController: this.sessionController, userController: this.userController, configuration: this.configuration, - progressBar: this.progressBar, } } @@ -194,45 +193,43 @@ class App { } } + sessionController = new Session() + + userController = new User() + state = { // app initialized: false, - isMobile: false, crash: false, - isOnTransition: false, // app session session: null, data: null, } - flushState = () => { - this.setState({ session: null, data: null }) - } - - isValidSession = async () => { - return await this.sessionController.isCurrentTokenValid() + flushState = async () => { + await this.setState({ session: null, data: null }) } componentDidMount = async () => { + this.eventBus.emit("app_loading") + + await this.contexts.app.initializeDefaultBridge() await this.initialization() + + this.eventBus.emit("app_ready") } initialization = async () => { try { - this.eventBus.emit("splash_show") - await this.contexts.app.initializeDefaultBridge() - await this.__init_session() - await this.__init_user() - this.eventBus.emit("app_ready") + await this.__SessionInit() + await this.__UserInit() } catch (error) { - this.eventBus.emit("splash_close") throw new ThrowCrash(error.message, error.description) } - this.eventBus.emit("splash_close") } - __init_session = async () => { + __SessionInit = async () => { if (typeof Session.token === "undefined") { window.app.eventBus.emit("forceToLogin") } else { @@ -252,7 +249,7 @@ class App { this.setState({ session: this.session }) } - __init_user = async () => { + __UserInit = async () => { if (!this.session || !this.session.valid) { return false } @@ -267,6 +264,10 @@ class App { } render() { + if (!this.state.initialized) { + return null + } + if (this.state.crash) { return
@@ -275,37 +276,26 @@ class App {

{this.state.crash.message}

{this.state.crash.error}
+
+ window.location.reload()}>Reload +
} - if (!this.state.initialized) { - return null - } - return ( {config.app.siteName} - - - - -
- -
- - - -
-
- - - + + + + + ) diff --git a/packages/app/src/components/AboutApp/index.jsx b/packages/app/src/components/AboutApp/index.jsx index 7b1bb656..cf841fb2 100644 --- a/packages/app/src/components/AboutApp/index.jsx +++ b/packages/app/src/components/AboutApp/index.jsx @@ -21,9 +21,9 @@ export class AboutCard extends React.Component { } render() { - const eviteNamespace = window.__evite + const eviteNamespace = window.__evite ?? {} const appConfig = config.app ?? {} - const isDevMode = eviteNamespace.env.NODE_ENV !== "production" + const isDevMode = eviteNamespace?.env?.NODE_ENV !== "production" return ( {appConfig.siteName}
- v{eviteNamespace.projectVersion} + v{eviteNamespace?.projectVersion ?? " experimental"} - eVite v{eviteNamespace.eviteVersion} - - v{eviteNamespace.versions.node} - - + {eviteNamespace.eviteVersion && + eVite v{eviteNamespace?.eviteVersion}} + {eviteNamespace.version?.node && + v{eviteNamespace?.versions?.node} + } {isDevMode ? : } {isDevMode ? "development" : "stable"} diff --git a/packages/app/src/components/ActionsBar/index.jsx b/packages/app/src/components/ActionsBar/index.jsx index bcda9a57..c998765b 100644 --- a/packages/app/src/components/ActionsBar/index.jsx +++ b/packages/app/src/components/ActionsBar/index.jsx @@ -3,9 +3,9 @@ import classnames from "classnames" import "./index.less" export default (props) => { - const { children, float } = props + const { children } = props - return
+ return
{children}
diff --git a/packages/app/src/components/ActionsBar/index.less b/packages/app/src/components/ActionsBar/index.less index d12e74d1..2eed8f70 100644 --- a/packages/app/src/components/ActionsBar/index.less +++ b/packages/app/src/components/ActionsBar/index.less @@ -1,28 +1,73 @@ +@actionsBar_height: fit-content; + .actionsBar_card { - border: 1px solid #e0e0e0; + display: inline-block; + white-space: nowrap; + + overflow-x: overlay; + padding: 15px; + + width: 100%; + height: @actionsBar_height; + + border: 1px solid #e0e0e0; border-radius: 8px; - transition: all 200ms ease-in-out; + background-color: #0c0c0c15; backdrop-filter: blur(10px); + transition: all 200ms ease-in-out; + + ::-webkit-scrollbar { + position: absolute; + display: none; + + width: 0; + height: 0; + z-index: 0; + } + &.float { z-index: 1000; position: sticky; + bottom: 0; top: 0; right: 0; width: 100%; } + + &.fixedBottom { + z-index: 1000; + position: fixed; + bottom: 0; + right: 0; + left: 0; + width: 100%; + margin-bottom: 10px; + } + + &.fixedTop { + z-index: 1000; + position: fixed; + top: 0; + right: 0; + left: 0; + width: 100%; + margin-bottom: 10px; + } } .actionsBar_flexWrapper { - transition: all 200ms ease-in-out; display: flex; flex-direction: row; align-items: center; + height: 100%; + transition: all 200ms ease-in-out; + > div { - margin-right: 10px; + margin: 0 5px; display: flex; flex-direction: column; align-items: center; diff --git a/packages/app/src/components/Clock/index.jsx b/packages/app/src/components/Clock/index.jsx new file mode 100644 index 00000000..dbb49d71 --- /dev/null +++ b/packages/app/src/components/Clock/index.jsx @@ -0,0 +1,17 @@ +import React from "react" + +import "./index.less" + +export default () => { + const [time, setTime] = React.useState(new Date()) + + React.useEffect(() => { + const interval = setInterval(() => { + setTime(new Date()) + }, 1000) + + return () => clearInterval(interval) + }, []) + + return
{time.toLocaleTimeString()}
+} \ No newline at end of file diff --git a/packages/app/src/components/FabricCreator/components/operations/index.less b/packages/app/src/components/Clock/index.less similarity index 100% rename from packages/app/src/components/FabricCreator/components/operations/index.less rename to packages/app/src/components/Clock/index.less diff --git a/packages/app/src/components/FabricCreator/components/index.js b/packages/app/src/components/FabricCreator/components/index.js deleted file mode 100644 index e38dafdd..00000000 --- a/packages/app/src/components/FabricCreator/components/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as Operations } from './operations' \ No newline at end of file diff --git a/packages/app/src/components/FabricCreator/components/operations/index.jsx b/packages/app/src/components/FabricCreator/components/operations/index.jsx deleted file mode 100644 index 8bed3b2b..00000000 --- a/packages/app/src/components/FabricCreator/components/operations/index.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react" -import * as antd from "antd" -import "./index.less" - -const api = window.app.request - -export default class Operations extends React.Component { - state = { - list: [] - } - - componentDidMount = async () => { - await this.loadOperations() - } - - loadOperations = async () => { - const operations = await api.get.operations() - console.log(operations) - } - - renderItem = (item) => { - console.log(item) - - return - {item} - - } - - render() { - return
- -
- } -} \ No newline at end of file diff --git a/packages/app/src/components/FabricCreator/index.jsx b/packages/app/src/components/FabricCreator/index.jsx deleted file mode 100644 index a893e7f5..00000000 --- a/packages/app/src/components/FabricCreator/index.jsx +++ /dev/null @@ -1,480 +0,0 @@ -import React from 'react' -import * as antd from 'antd' -import { Icons as FIcons, createIconRender } from "components/Icons" -import * as MDIcons from "react-icons/md" -import loadable from "@loadable/component" - -import "./index.less" - -const Icons = { - ...FIcons, - ...MDIcons -} - -const FormComponents = { - "input": antd.Input, - "textarea": antd.Input.TextArea, - "select": antd.Select, - "datepicker": antd.DatePicker, -} - -const requestModifyByType = { - "vaultItem": { - "additions": ["essc"] - } -} - -// FIELDS -const FieldsForms = { - description: { - label: "Description", - component: "input", - updateEvent: "onChange", - onUpdate: (update) => { - return update.target.value - }, - style: { - minWidth: "300px", - }, - props: { - placeholder: "Describe something...", - } - }, - operations: { - label: "Operations", - component: "input", - updateEvent: "onChange", - onUpdate: (update) => { - return update.target.value - }, - }, - location: { - label: "Location", - component: "select", - updateEvent: "onChange", - children: async () => { - const api = window.app.request - const regions = await api.get.regions() - - return regions.map(region => { - return {region.name} - }) - }, - props: { - placeholder: "Select a location", - } - }, - vaultItemTypeSelector: { - label: "Type", - component: "select", - updateEvent: "onChange", - children: async () => { - let types = await import("schemas/vaultItemsTypes.json") - - types = types.default || types - - return Object.keys(types).map((group) => { - return - {types[group].map((type) => { - return {String(type).toTitleCase()} - })} - - }) - }, - props: { - placeholder: "Select a type", - } - }, - vaultItemSerial: { - label: "Serial number", - component: "input", - updateEvent: "onChange", - onUpdate: (update) => { - return update.target.value - }, - props: { - placeholder: "S/N 00000000X", - } - }, - vaultItemManufacturer: { - label: "Manufacturer", - component: "input", - updateEvent: "onChange", - onUpdate: (update) => { - return update.target.value - }, - props: { - placeholder: "e.g. Hewlett Packard", - } - }, - vaultItemManufacturedYear: { - label: "Manufactured Year", - component: "datepicker", - updateEvent: "onChange", - onUpdate: (update) => { - return update.year() - }, - props: { - picker: "year" - } - }, -} - -//FORMULAS -const ProductFormula = { - defaultFields: [ - "description", - "operations", - ] -} - -const OperationFormula = { - defaultFields: [ - "description", - "task", - ] -} - -const PhaseFormula = { - defaultFields: [ - "description", - "task", - ] -} - -const TaskFormula = { - defaultFields: [ - "description", - "tasks", - ] -} - -const VaultItemFormula = { - defaultFields: [ - // TODO: include location - "vaultItemTypeSelector", - "vaultItemSerial", - "vaultItemManufacturer", - "vaultItemManufacturedYear", - "location", - ] -} - -const FORMULAS = { - product: ProductFormula, - operation: OperationFormula, - phase: PhaseFormula, - task: TaskFormula, - vaultItem: VaultItemFormula, -} - -// TYPES -const FabricItemTypesIcons = { - "product": "Box", - "operation": "Settings", - "phase": "GitCommit", - "task": "Tool", - "vaultItem": "Archive", -} - -const FabricItemTypes = ["product", "operation", "phase", "task", "vaultItem"] - -export default class FabricCreator extends React.Component { - state = { - loading: true, - submitting: false, - error: null, - - name: null, - type: null, - fields: [], - values: {}, - } - - componentDidMount = async () => { - await this.setItemType(this.props.defaultType ?? "product") - this.setState({ loading: false }) - } - - toogleLoading = (to) => { - this.setState({ loading: to ?? !this.state.loading }) - } - - toogleSubmitting = (to) => { - this.setState({ submitting: to ?? !this.state.submitting }) - } - - clearError = () => { - if (this.state.error != null) { - this.setState({ error: null }) - } - } - - clearValues = async () => { - await this.setState({ values: {} }) - } - - clearFields = async () => { - await this.setState({ fields: [] }) - } - - setItemType = async (type) => { - const formulaKeys = Object.keys(FORMULAS) - - if (formulaKeys.includes(type)) { - const formula = FORMULAS[type] - - await this.clearValues() - await this.clearFields() - - formula.defaultFields.forEach(field => { - this.appendFieldByType(field) - }) - - await this.setState({ type: type, name: "New item" }) - } else { - console.error(`Cannot load default fields from formula with type ${type}`) - } - } - - appendFieldByType = (fieldType) => { - const field = FieldsForms[fieldType] - - if (typeof field === "undefined") { - console.error(`No form available for field [${fieldType}]`) - return null - } - - const fields = this.state.fields - - if (this.fieldsHasTypeKey(fieldType) && !field.allowMultiple) { - console.error(`Field [${fieldType}] already exists, and only can exists 1`) - return false - } - - fields.push(this.generateFieldRender({ type: fieldType, ...field })) - - this.setState({ fields: fields }) - } - - fieldsHasTypeKey = (key) => { - let isOnFields = false - - const fields = this.state.fields - - fields.forEach(field => { - field.props.type === key ? isOnFields = true : null - }) - - return isOnFields - } - - renderFieldSelectorMenu = () => { - return { - this.appendFieldByType(e.key) - }} - > - {Object.keys(FieldsForms).map((key) => { - const field = FieldsForms[key] - const icon = field.icon && createIconRender(field.icon) - const disabled = this.fieldsHasTypeKey(key) && !field.allowMultiple - - return - {icon ?? null} - {key.charAt(0).toUpperCase() + key.slice(1)} - - })} - - } - - renderTypeMenuSelector = () => { - return { - this.setItemType(e.key) - }} - > - {FabricItemTypes.map((type) => { - const TypeIcon = FabricItemTypesIcons[type] && createIconRender(FabricItemTypesIcons[type]) - - return - {TypeIcon ?? null} - {type.charAt(0).toUpperCase() + type.slice(1)} - - })} - - } - - onDone = async () => { - this.clearError() - this.toogleSubmitting(true) - - const api = window.app.request - let properties = {} - - this.getProperties().forEach((property) => { - if (typeof properties[property.type] !== "undefined") { - return properties[property.id] = property.value - } - - return properties[property.type] = property.value - }) - - let payload = { - type: this.state.type, - name: this.state.name, - properties: properties, - } - - if (typeof requestModifyByType[this.state.type] !== "undefined") { - payload = { - ...payload, - ...requestModifyByType[this.state.type], - } - } - - await api.put.fabric(payload).catch((response) => { - console.error(response) - this.setState({ error: response }) - - return null - }) - - this.toogleSubmitting(false) - - if (!this.state.error && typeof this.props.close === "function") { - this.props.close() - } - } - - onChangeName = (event) => { - this.setState({ name: event.target.value }) - } - - onUpdateValue = (event, value) => { - const { updateEvent, key } = event - - let state = this.state - state.values[key] = value - - this.setState(state) - } - - removeField = (key) => { - let values = this.state.values - let fields = this.state.fields.filter(field => field.key != key) - - delete values[key] - - this.setState({ fields: fields, values: values }) - } - - getProperties = () => { - return this.state.fields.map((field) => { - return { - type: field.props.type, - id: field.props.id, - value: this.state.values[field.key], - } - }) - } - - getKeyFromLatestFieldType = (type) => { - let latestByType = 0 - - this.state.fields.forEach((field) => { - field.props.type === type ? latestByType++ : null - }) - - return `${type}-${latestByType}` - } - - generateFieldRender = (field) => { - if (!field.key) { - field.key = this.getKeyFromLatestFieldType(field.type) - } - - if (typeof FormComponents[field.component] === "undefined") { - console.error(`No component type available for field [${field.key}]`) - return null - } - - const getSubmittingState = () => { - return this.state.submitting - } - - let fieldComponentProps = { - ...field.props, - value: this.state.values[field.key], - disabled: getSubmittingState(), - [field.updateEvent]: (...args) => { - if (typeof field.onUpdate === "function") { - return this.onUpdateValue({ updateEvent: field.updateEvent, key: field.key }, field.onUpdate(...args)) - } - return this.onUpdateValue({ updateEvent: field.updateEvent, key: field.key }, ...args) - }, - } - - let RenderComponent = null - - if (typeof field.children === "function") { - RenderComponent = loadable(async () => { - try { - const children = await field.children() - return () => React.createElement(FormComponents[field.component], fieldComponentProps, children) - } catch (error) { - console.log(error) - return ()=>
- Load Error -
- } - }, { - fallback:
Loading...
, - }) - } else { - RenderComponent = () => React.createElement(FormComponents[field.component], fieldComponentProps) - } - - return
-
{ this.removeField(field.key) }}>
-

{field.icon && createIconRender(field.icon)}{field.label}

-
- -
-
- } - - render() { - if (this.state.loading) { - return - } - const TypeIcon = FabricItemTypesIcons[this.state.type] && createIconRender(FabricItemTypesIcons[this.state.type]) - - return
-
-
- - {TypeIcon ?? } - -
- -
- -
-
- {this.state.submitting ? : this.state.fields} -
-
- - - - - Done -
- {this.state.error &&
- {this.state.error} -
} -
-
- } -} diff --git a/packages/app/src/components/FabricCreator/index.less b/packages/app/src/components/FabricCreator/index.less deleted file mode 100644 index 2b769ff4..00000000 --- a/packages/app/src/components/FabricCreator/index.less +++ /dev/null @@ -1,122 +0,0 @@ -.fabric_creator { - .name { - display: flex; - align-items: center; - font-size: 45px; - - height: fit-content; - line-height: 0; - - input { - color: unset; - line-height: 0; - font-size: 45px; - height: fit-content; - transition: all 0.2s ease-in-out; - border: 0; - } - - input:focus { - color: #5e5e5e; - } - - svg { - margin: 0!important; - padding-right: 0!important; - } - - .type { - border-radius: 10px; - height: fit-content; - padding: 10px; - cursor: pointer; - } - - .type:hover{ - background-color: antiquewhite; - } - - margin-bottom: 20px; - } - - .fields { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - .wrap { - display: flex; - flex-wrap: wrap; - - width: 100%; - - .field { - margin: 10px; - width: fit-content; - - border-radius: 12px; - padding: 10px; - border: transparent dashed 1px; - - transition: all 150ms ease-out; - - min-width: 100px; - - .fieldContent { - min-width: 100px; - } - - .close { - color: #8d8d8dc4; - opacity: 0; - position: absolute; - transform: translate(-85%, -100%); - cursor: pointer; - transition: all 150ms ease-out; - } - } - - .field:hover { - border-color: #8d8d8dc4; - - .close { - opacity: 1; - } - } - } - - .bottom_actions { - display: flex; - justify-content: center; - align-items: center; - - width: 100px; - margin-top: 20px; - - font-size: 25px; - color: #5e5e5e; - border: transparent solid 1px; - border-radius: 8px; - - transition: all 150ms ease-in-out; - - svg { - margin: 0!important; - padding-right: 0!important; - } - } - - .bottom_actions:hover { - cursor: pointer; - border-color: #5e5e5e; - } - - .error { - color: red; - font-size: 12px; - margin-top: 5px; - transition: all 150ms ease-out; - } - } -} \ No newline at end of file diff --git a/packages/app/src/components/QRReader/index.jsx b/packages/app/src/components/QRReader/index.jsx deleted file mode 100644 index e46dacda..00000000 --- a/packages/app/src/components/QRReader/index.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react" -import QrReader from "react-qr-reader" -import { Window } from "components" - -export class Reader extends React.Component { - state = { - delay: 100, - result: "No result", - } - qrReaderRef = React.createRef() - - handleScan = (data) => { - this.setState({ - result: data, - }) - } - - handleError = (err) => { - console.error(err) - } - - openImageDialog = () => { - this.qrReaderRef.current.openImageDialog() - } - - render() { - const previewStyle = { - height: 240, - width: 320, - } - - return ( -
- -

{this.state.result}

- - -
- ) - } -} - -export function openModal() { - new Window.DOMWindow({ id: "QRScanner", children: Reader }).create() -} diff --git a/packages/app/src/components/ScheduledProgress/index.jsx b/packages/app/src/components/ScheduledProgress/index.jsx new file mode 100644 index 00000000..61e8293e --- /dev/null +++ b/packages/app/src/components/ScheduledProgress/index.jsx @@ -0,0 +1,85 @@ +import React from "react" +import moment from "moment" +import * as antd from "antd" +import classnames from "classnames" + +import "./index.less" + +const defaultDateFormat = "DD-MM-YYYY hh:mm" + +export default class ScheduledProgress extends React.Component { + isDateReached = (date) => { + const format = this.props.dateFormat ?? defaultDateFormat + const now = moment().format(format) + const result = moment(date, format).isSameOrBefore(moment(now, format)) + + console.debug(`[${date}] is before [${now}] => ${result}`) + + return result + } + + getDiffBetweenDates = (start, end) => { + // THIS IS NOT COUNTING WITH THE YEAR + const format = "DD-MM-YYYY" + + const startDate = moment(start, format) + const endDate = moment(end, format) + const now = moment().format(format) + + // count days will took to complete + const days = endDate.diff(startDate, "days") + + const daysLeft = endDate.diff(moment(now, format), "days") + const daysPassed = moment(now, format).diff(startDate, "days") + + let percentage = 0 + + switch (daysLeft) { + case 0: { + percentage = 99 + break + } + case 1: { + percentage = 95 + break + } + default: { + if (daysPassed > 0 && daysPassed < days) { + percentage = (daysPassed / days) * 100 + } + break + } + } + + if (daysPassed > days) { + percentage = 100 + } + + return { daysLeft, daysPassed, percentage } + } + + render() { + const startReached = this.isDateReached(this.props.start) + const finishReached = this.isDateReached(this.props.finish) + const datesDiff = this.getDiffBetweenDates(this.props.start, this.props.finish) + + return
+
+ {this.props.start} +
+ +
+ {this.props.finish} +
+
+ } +} \ No newline at end of file diff --git a/packages/app/src/components/ScheduledProgress/index.less b/packages/app/src/components/ScheduledProgress/index.less new file mode 100644 index 00000000..bb060934 --- /dev/null +++ b/packages/app/src/components/ScheduledProgress/index.less @@ -0,0 +1,68 @@ +.scheduled_progress { + display: flex; + flex-direction: row; + justify-content: center; + + .point { + height: 40px; + text-align: center; + align-items: end; + white-space: nowrap; + display: flex; + width: 72px; + justify-content: center; + + &.right { + transform: translate(-50%, 0); + } + + &.left { + transform: translate(50%, 0); + } + + &.reached { + color: rgba(0, 0, 0, 0.45); + } + } + + .ant-progress::before { + content: ""; + width: 8px; + height: 8px; + border-radius: 24px; + transform: translateX(-10px); + background-color: rgb(128, 128, 128); + } + + .ant-progress::after { + content: ""; + width: 8px; + height: 8px; + border-radius: 24px; + transform: translateX(10px); + background-color: rgb(128, 128, 128); + } + + .ant-progress { + font-size: 0; + display: flex; + align-items: center; + height: fit-content; + + &.startReached { + &::before { + background-color: var(--primaryColor) !important; + } + } + + &.finishReached { + &::after { + background-color: var(--primaryColor) !important; + } + } + } + + .ant-progress-bg { + background-color: var(--primaryColor); + } +} diff --git a/packages/app/src/components/SelectableList/index.jsx b/packages/app/src/components/SelectableList/index.jsx index b0d1f912..d2ecc24c 100644 --- a/packages/app/src/components/SelectableList/index.jsx +++ b/packages/app/src/components/SelectableList/index.jsx @@ -1,6 +1,5 @@ import React from "react" import { Icons } from "components/Icons" -import { ActionsBar } from "components" import { List, Button } from "antd" import classnames from "classnames" @@ -27,6 +26,12 @@ export default class SelectableList extends React.Component { } } + unselectAll = () => { + this.setState({ + selectedKeys: [], + }) + } + selectKey = (key) => { let list = this.state.selectedKeys ?? [] list.push(key) @@ -44,9 +49,7 @@ export default class SelectableList extends React.Component { this.props.onDone(this.state.selectedKeys) } - this.setState({ - selectedKeys: [], - }) + this.unselectAll() } onDiscard = () => { @@ -54,9 +57,7 @@ export default class SelectableList extends React.Component { this.props.onDiscard(this.state.selectedKeys) } - this.setState({ - selectedKeys: [], - }) + this.unselectAll() } componentDidUpdate(prevProps, prevState) { @@ -69,131 +70,131 @@ export default class SelectableList extends React.Component { } } - renderActions = () => { - if (typeof this.props.renderActions !== "undefined" && !this.props.renderActions) { - return false - } - if (this.state.selectedKeys.length === 0) { - return false - } + renderProvidedActions = () => { + return this.props.actions.map((action) => { + return ( +
+ -
- ) - }) - } + + this.props[action.props.call](data) + } + } + }} + > + {action} + +
+ ) + }) + } + + renderActions = () => { + if (this.props.actionsDisabled) { return null } - return ( -
- -
- -
- {renderProvidedActions()} -
+ return
+
+
- ) + {Array.isArray(this.props.actions) && this.renderProvidedActions()} +
+ } + + isKeySelected = (key) => { + return this.state.selectedKeys.includes(key) + } + + renderItem = (item) => { + if (item.children) { + return
+ {item.label} +
+ {item.children.map((subItem) => { + return this.renderItem(subItem) + })} +
+
+ } + + const renderChildren = this.props.renderItem(item) + + const _key = item.key ?? item.id ?? item._id ?? renderChildren.key + + const selectionMethod = ["onClick", "onDoubleClick"].includes(this.props.selectionMethod) ? this.props.selectionMethod : "onClick" + const isSelected = this.isKeySelected(_key) + const isDisabled = renderChildren.props.disabled + const isNotSelectable = renderChildren.props.notSelectable + + let renderProps = { + disabled: isDisabled, + children: renderChildren, + className: classnames("selectableList_item", { + ["selected"]: isSelected && !isNotSelectable, + ["disabled"]: isDisabled && !isNotSelectable, + }), + [selectionMethod]: () => { + if (isDisabled && isNotSelectable) { + return false + } + if (typeof this.props.selectionEnabled !== "undefined") { + if (!Boolean(this.props.selectionEnabled)) { + return false + } + } + + if (isSelected) { + this.unselectKey(_key) + } else { + this.selectKey(_key) + } + } + } + + if (selectionMethod == "onDoubleClick") { + renderProps.onClick = () => { + if (this.state.selectedKeys.length > 0) { + if (isSelected) { + this.unselectKey(_key) + } + } + } + } + + return
} render() { - const validSelectionMethods = ["onClick", "onDoubleClick"] - - const renderMethod = (item) => { - const selectionMethod = validSelectionMethods.includes(this.props.selectionMethod) ? this.props.selectionMethod : "onClick" - - if (typeof this.props.renderItem === "function") { - const _key = item.key ?? item.id ?? item._id - const list = this.state.selectedKeys - const isSelected = list.includes(_key) - - let props = { - key: _key, - id: _key, - className: classnames("selectableList_item", this.props.itemClassName, { - selected: this.state.selectedKeys.includes(_key), - }), - [selectionMethod]: () => { - if (typeof this.props.selectionEnabled !== "undefined") { - if (!Boolean(this.props.selectionEnabled)) { - return false - } - } - - if (isSelected) { - this.unselectKey(_key) - } else { - this.selectKey(_key) - } - } - } - - if (selectionMethod == "onDoubleClick") { - props.onClick = () => { - if (list.length > 0) { - if (isSelected) { - this.unselectKey(_key) - } - } - } - } - - return ( -
- {this.props.renderItem(item)} -
- ) - } - - console.warn("renderItem method is not defined!") - return null - } - - const { borderer, grid, header, loadMore, locale, pagination, rowKey, size, split, itemLayout, loading } = - this.props + const { borderer, grid, header, loadMore, locale, pagination, rowKey, size, split, itemLayout, loading } = this.props const listProps = { borderer, grid, @@ -208,18 +209,25 @@ export default class SelectableList extends React.Component { loading, } - return ( -
- - {this.renderActions()} + if (this.state.selectedKeys.length === 0) { + if (window.isMobile && !this.props.ignoreMobileActions) { + window.app.BottomBarController.clear() + } + } else { + if (window.isMobile && !this.props.ignoreMobileActions) { + window.app.BottomBarController.render(this.renderActions()) + } + } + + return
+ +
+ {this.props.ignoreMobileActions && this.renderActions()}
- ) +
} } \ No newline at end of file diff --git a/packages/app/src/components/SelectableList/index.less b/packages/app/src/components/SelectableList/index.less index 3fe722a2..0d14d7d1 100644 --- a/packages/app/src/components/SelectableList/index.less +++ b/packages/app/src/components/SelectableList/index.less @@ -6,9 +6,9 @@ .selectableList_item { cursor: pointer; - border: rgba(51, 51, 51, 0.3) 1px solid; - border-radius: 8px; - margin-bottom: 12px; + border: rgba(51, 51, 51, 0.3) 1px solid; + border-radius: 8px; + margin-bottom: 12px; h1 { user-select: none; @@ -24,42 +24,97 @@ } } +.selectableList_group { + display: flex; + flex-direction: column; + + .selectableList_subItems { + margin-left: 10px; + } + + margin-bottom: 10px; +} + .selectableList_item { + display: inline-flex; + overflow-x: overlay; + + align-items: center; + user-select: none; width: 100%; + height: fit-content; border: @selectableList_item_borderColor_normal 1px solid; - border-radius: 4px; - - margin-bottom: 6px; - padding: 10px; - - transition: all 150ms ease-in-out; + border-radius: 4px; - > div { - margin: 7px; - } + margin-bottom: 6px; + padding: 7px; + + transition: all 150ms ease-in-out; &.selected { background-color: #f5f5f5; transform: translate(10px, 0); } + + &.disabled { + opacity: 0.5; + pointer-events: none; + } + + ::-webkit-scrollbar { + position: absolute; + display: none; + + width: 0; + height: 0; + z-index: 0; + } } -.bottomActions_wrapper { +.selectableList_bottomActions_wrapper { position: sticky; - z-index: 1000; + z-index: 300; + + left: 0; + bottom: 0; width: 100%; - - bottom: 0; - left: 0; - right: 0; - - border-radius: 0; display: flex; flex-direction: row; + align-items: center; justify-content: center; } + +.selectableList_bottomActions { + width: fit-content; + display: inline-flex; + + align-items: center; + justify-content: center; + + padding: 12px; + + border: 1px solid #e0e0e0; + border-radius: 8px; + + background-color: #0c0c0c15; + backdrop-filter: blur(10px); + + transition: all 200ms ease-in-out; + + > div { + margin: 0 5px; + } + + &.mobile { + background-color: transparent; + backdrop-filter: none; + width: 100%; + padding: 0; + border: none; + } +} diff --git a/packages/app/src/components/Settings/index.jsx b/packages/app/src/components/Settings/index.jsx index 9c78fd14..3ec20d53 100644 --- a/packages/app/src/components/Settings/index.jsx +++ b/packages/app/src/components/Settings/index.jsx @@ -87,6 +87,10 @@ export default class SettingsMenu extends React.Component { item.props.onClick = (event) => this.handleEvent(event, item) break } + case "select": { + item.props.onChange = (event) => this.handleEvent(event, item) + break + } default: { if (!item.props.children) { item.props.children = item.title ?? item.id @@ -101,10 +105,11 @@ export default class SettingsMenu extends React.Component {
-
- {item.icon ? React.createElement(Icons[item.icon]) : null} +

+ {Icons[item.icon] ? React.createElement(Icons[item.icon]) : null} {item.title ?? item.id} -

+ +

{item.description}

{item.experimental && Experimental } @@ -151,7 +156,7 @@ export default class SettingsMenu extends React.Component { } render() { - const isDevMode = window.__evite.env.NODE_ENV !== "production" + const isDevMode = window.__evite?.env?.NODE_ENV !== "production" return (
@@ -161,7 +166,7 @@ export default class SettingsMenu extends React.Component {
{config.app?.siteName}
- v{window.__evite.projectVersion} + v{window.__evite?.projectVersion}
diff --git a/packages/app/src/components/Settings/index.less b/packages/app/src/components/Settings/index.less index d9de886b..62d6a402 100644 --- a/packages/app/src/components/Settings/index.less +++ b/packages/app/src/components/Settings/index.less @@ -28,7 +28,13 @@ display: flex; align-items: center; - h5{ + h4{ + margin: 0; + } + + p { + font-size: 11px; + color: var(--background-color-contrast); margin: 0; } diff --git a/packages/app/src/components/UserSelector/index.jsx b/packages/app/src/components/UserSelector/index.jsx new file mode 100644 index 00000000..0fcdc655 --- /dev/null +++ b/packages/app/src/components/UserSelector/index.jsx @@ -0,0 +1,120 @@ +import React from "react" +import * as antd from "antd" +import { Icons } from "components/Icons" +import { SelectableList } from "components" +import { debounce } from "lodash" +import fuse from "fuse.js" + +import "./index.less" + +export default class UserSelector extends React.Component { + state = { + loading: true, + data: [], + searchValue: null, + } + + api = window.app.request + + componentDidMount = async () => { + this.toogleLoading(true) + await this.fetchUsers() + } + + toogleLoading = (to) => { + this.setState({ loading: to ?? !this.state.loading }) + } + + fetchUsers = async () => { + const data = await this.api.get.users(undefined, { select: this.props.select }).catch((err) => { + console.error(err) + antd.message.error("Error fetching operators") + }) + + this.setState({ data: data, loading: false }) + } + + isExcludedId = (id) => { + if (this.props.excludedIds) { + return this.props.excludedIds.includes(id) + } + + return false + } + + renderItem = (item) => { + return
+
+ +
+
+

{item.fullName ?? item.username}

+
+
+ } + + search = (value) => { + if (typeof value !== "string") { + if (typeof value.target?.value === "string") { + value = value.target.value + } + } + + if (value === "") { + return this.setState({ searchValue: null }) + } + + const searcher = new fuse(this.state.data, { + includeScore: true, + keys: ["username", "fullName"], + }) + + const result = searcher.search(value) + + this.setState({ + searchValue: result.map((entry) => { + return entry.item + }), + }) + } + + debouncedSearch = debounce((value) => this.search(value), 500) + + onSearch = (event) => { + if (event === "" && this.state.searchValue) { + return this.setState({ searchValue: null }) + } + + this.debouncedSearch(event.target.value) + } + + render() { + if (this.state.loading) { + return + } + + return
+
+
+ +
+
+ + Done +
+ ]} + onDone={(keys) => this.props.handleDone(keys)} + /> +
+ } +} \ No newline at end of file diff --git a/packages/app/src/components/UserSelector/index.less b/packages/app/src/components/UserSelector/index.less new file mode 100644 index 00000000..fd95cd48 --- /dev/null +++ b/packages/app/src/components/UserSelector/index.less @@ -0,0 +1,19 @@ +.users_selector { + .header { + margin-bottom: 10px; + } + + .item { + display: flex; + flex-direction: row; + align-items: center; + + h1 { + margin: 0; + } + + > div { + margin-right: 8px; + } + } +} diff --git a/packages/app/src/components/index.js b/packages/app/src/components/index.js index 478f1bcc..b8c551ca 100644 --- a/packages/app/src/components/index.js +++ b/packages/app/src/components/index.js @@ -8,10 +8,11 @@ export { default as Sessions } from "./Sessions" export { default as ActionsBar } from "./ActionsBar" export { default as SelectableList } from "./SelectableList" export { default as ObjectInspector } from "./ObjectInspector" -export { default as FabricCreator } from "./FabricCreator" export { default as ServerStatus } from "./ServerStatus" export { default as ModifierTag } from "./ModifierTag" +export { default as UserSelector } from "./UserSelector" +export { default as Clock } from "./Clock" +export { default as ScheduledProgress } from "./ScheduledProgress" export * as AboutApp from "./AboutApp" -export * as QRReader from "./QRReader" export * as Window from "./RenderWindow" \ No newline at end of file diff --git a/packages/app/src/controllers/index.js b/packages/app/src/controllers/index.js deleted file mode 100644 index a7b0a4a3..00000000 --- a/packages/app/src/controllers/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as SettingsController } from './settings' -export { default as SidebarController } from './sidebar' \ No newline at end of file diff --git a/packages/app/src/extensions/render/index.jsx b/packages/app/src/extensions/render/index.jsx index f9ec0571..6fdac022 100644 --- a/packages/app/src/extensions/render/index.jsx +++ b/packages/app/src/extensions/render/index.jsx @@ -1,82 +1,101 @@ import React from "react" import loadable from "@loadable/component" -import resolve from "pages" +import routes from "virtual:generated-pages" +import progressBar from "nprogress" + +import NotFound from "./statics/404" export const ConnectWithApp = (component) => { return window.app.bindContexts(component) } export function GetRoutesMap() { - const jsxFiles = import.meta.glob('/src/pages/**/**.jsx') - const tsxFiles = import.meta.glob('/src/pages/**/**.tsx') + return routes.map((route) => { + const { path } = route + route.name = + path + .replace(/^\//, "") + .replace(/:/, "") + .replace(/\//, "-") + .replace("all(.*)", "not-found") || "home" - return { ...jsxFiles, ...tsxFiles } + route.path = route.path.includes("*") ? "*" : route.path + + return route + }) +} + +export function GetRoutesComponentMap() { + return routes.reduce((acc, route) => { + const { path, component } = route + + acc[path] = component + + return acc + }, {}) +} + +export class RouteRender extends React.Component { + state = { + routes: GetRoutesComponentMap() ?? {}, + error: null, + } + + lastLocation = null + + componentDidMount() { + window.app.eventBus.on("locationChange", (event) => { + console.debug("[App] LocationChange, forcing update render...") + + // render controller needs an better method for update render, this is a temporary solution + // FIXME: this event is called multiple times. we need to debug them methods + if (typeof this.forceUpdate === "function") { + this.forceUpdate() + } + }) + } + + componentDidCatch(info, stack) { + this.setState({ error: { info, stack } }) + } + + // shouldComponentUpdate(nextProps, nextState) { + // if (this.lastLocation.pathname !== window.location.pathname) { + // return true + // } + // return false + // } + + render() { + this.lastLocation = window.location + + let path = this.props.path ?? window.location.pathname + let componentModule = this.state.routes[path] ?? this.props.staticRenders.NotFound ?? NotFound + + console.debug(`[RouteRender] Rendering ${path}`) + + if (this.state.error) { + if (this.props.staticRenders?.RenderError) { + return React.createElement(this.props.staticRenders?.RenderError, { error: this.state.error }) + } + + return JSON.stringify(this.state.error) + } + + return React.createElement(ConnectWithApp(componentModule), this.props) + } } export const LazyRouteRender = (props) => { const component = loadable(async () => { - const location = window.location - let path = props.path ?? location.pathname + // TODO: Support evite async component initializations - if (path.startsWith("/")) { - path = path.substring(1) - } - - const src = resolve(path) - console.log(src) - - let module = await import(src).catch((err) => { - console.error(err) - return props.staticRenders?.NotFound ?? import("./statics/404") - }) - module = module.default || module - - return class extends React.PureComponent { - state = { - error: null - } - - componentDidCatch(info, stack) { - this.setState({ error: { info, stack } }) - } - - render() { - if (this.state.error) { - if (props.staticRenders?.RenderError) { - return React.createElement(props.staticRenders?.RenderError, { error: this.state.error }) - } - - return JSON.stringify(this.state.error) - } - - return React.createElement(ConnectWithApp(module), props) - } - } + return RouteRender }) return React.createElement(component) } -export class RenderRouter extends React.Component { - lastPathname = null - lastHistoryState = null - - shouldComponentUpdate() { - if (this.lastPathname !== window.location.pathname || this.lastHistoryState !== window.app.history.location.state) { - return true - } - - return false - } - - render() { - this.lastPathname = window.location.pathname - this.lastHistoryState = window.app.history.location.state - - return LazyRouteRender({ ...this.props, path: this.lastPathname }) - } -} - export const extension = { key: "customRender", expose: [ @@ -125,25 +144,30 @@ export const extension = { async (app, main) => { const defaultTransitionDelay = 150 + main.progressBar = progressBar.configure({ parent: "html", showSpinner: false }) + main.history.listen((event) => { - main.eventBus.emit("setLocationDone") + main.eventBus.emit("transitionDone", event) + main.eventBus.emit("locationChange", event) + main.progressBar.done() }) - main.history.setLocation = (to, state) => { + main.history.setLocation = (to, state, delay) => { const lastLocation = main.history.lastLocation if (typeof lastLocation !== "undefined" && lastLocation?.pathname === to && lastLocation?.state === state) { return false } - main.eventBus.emit("setLocation") + main.progressBar.start() + main.eventBus.emit("transitionStart", delay) setTimeout(() => { main.history.push({ pathname: to, }, state) main.history.lastLocation = main.history.location - }, defaultTransitionDelay) + }, delay ?? defaultTransitionDelay) } main.setToWindowContext("setLocation", main.history.setLocation) diff --git a/packages/app/src/extensions/sound/index.js b/packages/app/src/extensions/sound/index.js index b87908de..3f7ced6a 100644 --- a/packages/app/src/extensions/sound/index.js +++ b/packages/app/src/extensions/sound/index.js @@ -1,4 +1,5 @@ import { Howl } from "howler" +import config from "config" export class SoundEngine { constructor() { @@ -10,10 +11,8 @@ export class SoundEngine { } getSounds = async () => { - const origin = process.env.NODE_ENV === "development" ? `${window.location.origin}/src/assets/sounds/index.js` : `${window.location.origin}/assets/sounds/index.js` - - let soundPack = await import(origin) - soundPack = soundPack.default || soundPack + // TODO: Load custom soundpacks manifests + let soundPack = config.defaultSoundPack ?? {} Object.keys(soundPack).forEach((key) => { const src = soundPack[key] @@ -29,6 +28,8 @@ export class SoundEngine { play = (name) => { if (this.sounds[name]) { this.sounds[name].play() + } else { + console.error(`Sound ${name} not found.`) } } } diff --git a/packages/app/src/i18n/index.js b/packages/app/src/i18n/index.js new file mode 100644 index 00000000..c5fde276 --- /dev/null +++ b/packages/app/src/i18n/index.js @@ -0,0 +1,57 @@ +import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from './locales' +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' + +export { + DEFAULT_LOCALE, + SUPPORTED_LOCALES, + SUPPORTED_LANGUAGES, + extractLocaleFromPath, +} from './locales' + +// This is a dynamic import so not all languages are bundled in frontend. +// For YAML format, install `@rollup/plugin-yaml`. +const messageImports = import.meta.glob('./translations/*.json') + +function importLocale(locale) { + const [, importLocale] = + Object.entries(messageImports).find(([key]) => + key.includes(`/${locale}.`) + ) || [] + + return importLocale && importLocale() +} + +export async function loadAsyncLanguage(i18n, locale = DEFAULT_LOCALE) { + try { + const result = await importLocale(locale) + if (result) { + i18n.addResourceBundle(locale, 'translation', result.default || result) + i18n.changeLanguage(locale) + } + } catch (error) { + console.error(error) + } +} + +export async function installI18n(locale = '') { + locale = SUPPORTED_LOCALES.includes(locale) ? locale : DEFAULT_LOCALE + const messages = await importLocale(locale) + + i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + // debug: true, + resources: { + // @ts-ignore + [locale]: { translation: messages.default || messages }, + }, + lng: locale, + fallbackLng: DEFAULT_LOCALE, + interpolation: { + escapeValue: false, // react already safes from xss + }, + }) +} + +export default i18n \ No newline at end of file diff --git a/packages/app/src/i18n/locales.js b/packages/app/src/i18n/locales.js new file mode 100644 index 00000000..6b6a7e6c --- /dev/null +++ b/packages/app/src/i18n/locales.js @@ -0,0 +1,18 @@ +export const SUPPORTED_LANGUAGES = [ + { + locale: 'en', + name: 'English', + default: true, + }, +] + +export const SUPPORTED_LOCALES = SUPPORTED_LANGUAGES.map((l) => l.locale) + +export const DEFAULT_LANGUAGE = SUPPORTED_LANGUAGES.find((l) => l.default) + +export const DEFAULT_LOCALE = DEFAULT_LANGUAGE?.locale + +export function extractLocaleFromPath(path = '') { + const [_, maybeLocale] = path.split('/') + return SUPPORTED_LOCALES.includes(maybeLocale) ? maybeLocale : DEFAULT_LOCALE +} \ No newline at end of file diff --git a/packages/app/src/i18n/translations/en.json b/packages/app/src/i18n/translations/en.json new file mode 100644 index 00000000..882f0b28 --- /dev/null +++ b/packages/app/src/i18n/translations/en.json @@ -0,0 +1,88 @@ +{ + "main": { + "welcome": "Welcome back, {{username}}", + "assigned_for_you": "Assigned for you", + }, + "generics": { + "new": "New", + "close": "Close", + "done": "Done", + "edit": "Edit", + "select": "Select", + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "import": "Import", + "export": "Export", + "refresh": "Refresh", + "reload": "Reload", + "search": "Search", + "status": "Status", + "type": "Type", + "about": "About", + "current": "Current", + "statistics": "Statistics", + "name": "Name", + "username": "Username", + "email": "Email", + "password": "Password", + "sessions": "Sessions", + "compact_view": "Compact view", + "about_us": "About us", + "sign_in": "Sign in", + "sign_out": "Sign out", + "sign_up": "Sign up", + "all_sessions": "All sessions", + "destroy_all_sessions": "Destroy all sessions", + "account_info": "Account information", + "password_confirmation": "Password confirmation", + }, + "fabric": { + "new_product": "New product", + "description": "Description", + "describe_something": "Describe something...", + "operations": "Operations", + "select_operation": "Select operation", + "select_operations": "Select operations...", + "tasks": "Tasks", + "select_task": "Select task", + "select_tasks": "Select tasks...", + "add_task": "Add task", + "add_tasks": "Add tasks", + "location": "Location", + "select_location": "Select location", + "select_locations": "Select locations...", + "add_location": "Add location", + "add_locations": "Add locations", + "variants": "Variants", + "select_variant": "Select variant", + "select_variants": "Select variants..." + }, + "settings": { + "general": { + "groupLabel": "General", + "language": "Language", + "persistentSession:": "Save session", + "persistentSession_description": "The application will save the session and no expire it." + }, + "sidebar": { + "groupLabel": "Sidebar", + "edit": "Edit sidebar", + "edit_description": "Edit the sidebar to add or remove items.", + "edit_button": "Edit", + "autoCollapse": "Auto collapse", + "autoCollapse_description": "Collapse the sidebar when loose focus" + }, + "aspect": { + "groupLabel": "Aspect", + "reduceAnimation": "Reduce animation", + "reduceAnimation_description": "Reduce the animation of the application.", + "darkMode": "Dark mode", + "darkMode_description": "Enable dark mode for the application.", + "primaryColor": "Primary color", + "primaryColor_description": "Change the primary color of the application.", + "resetTheme": "Reset theme", + "resetTheme_description": "Reset the theme to the default." + } + }, +} \ No newline at end of file diff --git a/packages/app/src/layout/bottombar/index.jsx b/packages/app/src/layout/bottombar/index.jsx new file mode 100644 index 00000000..a1d947b9 --- /dev/null +++ b/packages/app/src/layout/bottombar/index.jsx @@ -0,0 +1,68 @@ +import React from "react" +import * as antd from "antd" +import { createIconRender } from "components/Icons" + +import "./index.less" + +export default class BottomBar extends React.Component { + state = { + render: null + } + + componentDidMount = () => { + window.app.BottomBarController = { + render: (fragment) => { + this.setState({ render: fragment }) + }, + clear: () => { + this.setState({ render: null }) + }, + } + } + + onClickItemId = (id) => { + window.app.setLocation(`/${id}`) + } + + render() { + if (this.state.render) { + return
+ {this.state.render} +
+ } + + return
+
+
window.app.openFabric()} key="fabric" id="fabric" className="item"> +
+ {createIconRender("PlusCircle")} +
+
+
window.app.goMain()} key="main" id="main" className="item"> +
+ {createIconRender("Home")} +
+
+
this.onClickItemId("nav")} key="nav" id="nav" className="item"> +
+ {createIconRender("Navigation")} +
+
+
window.app.openSettings()} key="settings" id="settings" className="item"> +
+ {createIconRender("Settings")} +
+
+ {this.props.user ?
window.app.goToAccount()} key="account" id="account" className="item"> +
+ +
+
:
this.onClickItemId("login")} className="item"> +
+ {createIconRender("LogIn")} +
+
} +
+
+ } +} \ No newline at end of file diff --git a/packages/app/src/layout/bottombar/index.less b/packages/app/src/layout/bottombar/index.less new file mode 100644 index 00000000..3663c033 --- /dev/null +++ b/packages/app/src/layout/bottombar/index.less @@ -0,0 +1,53 @@ +@bottomBar_height: 80px; + +.bottomBar { + position: sticky; + z-index: 9999; + + left: 0; + bottom: 0; + + width: 100vw; + + display: flex; + flex-direction: row; + + align-items: center; + justify-content: center; + + background-color: var(--background-color-accent); + border-radius: 12px 12px 0 0; + + height: @bottomBar_height; + + padding: 10px; + + .items { + display: inline-block; + white-space: nowrap; + overflow-x: overlay; + + height: 100%; + + > div { + display: inline-block; + + height: 100%; + margin: 0 17px; + } + + .item { + .icon { + height: 100%; + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + color: var(--background-color-contrast); + font-size: 2rem; + } + } + } +} \ No newline at end of file diff --git a/packages/app/src/layout/drawer/index.jsx b/packages/app/src/layout/drawer/index.jsx index bc5685f1..38030e67 100644 --- a/packages/app/src/layout/drawer/index.jsx +++ b/packages/app/src/layout/drawer/index.jsx @@ -1,9 +1,105 @@ import React from "react" import * as antd from "antd" +import classnames from "classnames" import EventEmitter from "@foxify/events" +import { Icons } from "components/Icons" import "./index.less" +export default class DrawerController extends React.Component { + constructor(props) { + super(props) + this.state = { + addresses: {}, + refs: {}, + drawers: [], + } + + this.DrawerController = { + open: this.open, + close: this.close, + closeAll: this.closeAll, + } + + window.app["DrawerController"] = this.DrawerController + } + + sendEvent = (id, ...context) => { + const ref = this.state.refs[id]?.current + return ref.events.emit(...context) + } + + open = (id, component, options) => { + const refs = this.state.refs ?? {} + const drawers = this.state.drawers ?? [] + const addresses = this.state.addresses ?? {} + + const instance = { + id, + key: id, + ref: React.createRef(), + children: component, + options, + controller: this, + } + + if (typeof addresses[id] === "undefined") { + drawers.push() + addresses[id] = drawers.length - 1 + refs[id] = instance.ref + } else { + const ref = refs[id].current + const isLocked = ref.state.locked + + if (!isLocked) { + drawers[addresses[id]] = + refs[id] = instance.ref + } else { + console.warn("Cannot update an locked drawer.") + } + } + + this.setState({ refs, addresses, drawers }) + } + + destroy = (id) => { + let { addresses, drawers, refs } = this.state + const index = addresses[id] + + if (typeof drawers[index] !== "undefined") { + drawers = drawers.filter((value, i) => i !== index) + } + delete addresses[id] + delete refs[id] + + this.setState({ addresses, drawers }) + } + + close = (id) => { + const ref = this.state.refs[id]?.current + + if (typeof ref !== "undefined") { + if (ref.state.locked && ref.state.visible) { + return console.warn("This drawer is locked and cannot be closed") + } else { + return ref.close() + } + } else { + return console.warn("This drawer not exists") + } + } + + closeAll = () => { + this.state.drawers.forEach((drawer) => { + drawer.ref.current.close() + }) + } + + render() { + return this.state.drawers + } +} + export class Drawer extends React.Component { options = this.props.options ?? {} events = new EventEmitter() @@ -83,113 +179,25 @@ export class Drawer extends React.Component { handleFail: this.handleFail, } + if (window.isMobile) { + drawerProps.height = "100%" + drawerProps.placement = "bottom" + } + return ( - -
+ + {!this.props.headerDisabled &&
} subTitle={this.props.subtitle} /> -
+
}
{React.createElement(this.props.children, componentProps)}
) } -} - -export default class DrawerController extends React.Component { - constructor(props) { - super(props) - this.state = { - addresses: {}, - refs: {}, - drawers: [], - } - - this.DrawerController = { - open: this.open, - close: this.close, - closeAll: this.closeAll, - } - - window.app["DrawerController"] = this.DrawerController - } - - sendEvent = (id, ...context) => { - const ref = this.state.refs[id]?.current - return ref.events.emit(...context) - } - - open = (id, component, options) => { - const refs = this.state.refs ?? {} - const drawers = this.state.drawers ?? [] - const addresses = this.state.addresses ?? {} - - const instance = { - id, - ref: React.createRef(), - children: component, - options, - controller: this, - } - - if (typeof addresses[id] === "undefined") { - drawers.push() - addresses[id] = drawers.length - 1 - refs[id] = instance.ref - } else { - const ref = refs[id].current - const isLocked = ref.state.locked - - if (!isLocked) { - drawers[addresses[id]] = - refs[id] = instance.ref - } else { - console.warn("Cannot update an locked drawer.") - } - } - - this.setState({ refs, addresses, drawers }) - } - - destroy = (id) => { - let { addresses, drawers, refs } = this.state - const index = addresses[id] - - if (typeof drawers[index] !== "undefined") { - drawers = drawers.filter((value, i) => i !== index) - } - delete addresses[id] - delete refs[id] - - this.setState({ addresses, drawers }) - } - - close = (id) => { - const ref = this.state.refs[id]?.current - - if (typeof ref !== "undefined") { - if (ref.state.locked && ref.state.visible) { - return console.warn("This drawer is locked and cannot be closed") - } else { - return ref.close() - } - } else { - return console.warn("This drawer not exists") - } - } - - closeAll = () => { - this.state.drawers.forEach((drawer) => { - drawer.ref.current.close() - }) - } - - render() { - return this.state.drawers - } -} +} \ No newline at end of file diff --git a/packages/app/src/layout/drawer/index.less b/packages/app/src/layout/drawer/index.less index 122831cf..0d2295fd 100644 --- a/packages/app/src/layout/drawer/index.less +++ b/packages/app/src/layout/drawer/index.less @@ -1,20 +1,52 @@ -.drawer { - height: 100vh; - max-height: 100vh; +@import "theme/vars.less"; - .header { - position: relative; - top: 0; - z-index: 100; - } - .body { - padding: 10px 30px; - height: fit-content; - width: 100%; - } +.drawer { + height: 100vh; + max-height: 100vh; + + .pageTitle { + position: sticky; + background-color: var(--background-color-primary); + top: 0; + z-index: 100; + height: @app_header_height; + + padding: 10px 20px; + + .ant-page-header{ + height: 100%; + padding: 0; + } + .ant-page-header-heading{ + height: 100%; + margin: 0!important; + } + .ant-page-header-heading-left { + height: 100%; + margin: 0!important; + } + } + + .body { + z-index: 99; + + position: relative; + + padding: 20px 30px; + height: 100vh - @app_header_height; + width: 100%; + } + + &.mobile { + .body { + padding: 20px 15px; + } + } } -.ant-drawer-content, .ant-drawer-wrapper-body, .ant-drawer-body{ - height: 100vh; - max-height: 100vh; -} \ No newline at end of file +.ant-drawer-content, +.ant-drawer-wrapper-body, +.ant-drawer-body { + height: 100vh; + max-height: 100vh; +} diff --git a/packages/app/src/layout/header/index.jsx b/packages/app/src/layout/header/index.jsx index e0f4c628..a30d5b12 100644 --- a/packages/app/src/layout/header/index.jsx +++ b/packages/app/src/layout/header/index.jsx @@ -16,6 +16,10 @@ export default class Header extends React.Component { this.HeaderController = { toogleVisible: (to) => { + if (window.isMobile) { + to = true + } + this.setState({ visible: to ?? !this.state.visible }) }, isVisible: () => this.state.visible, @@ -28,15 +32,33 @@ export default class Header extends React.Component { window.app.openFabric() } + onClickHome = () => { + window.app.goMain() + } + render() { return ( - + + {window.isMobile &&
+ } + /> +
}
- -
-
- } style={{ display: "flex", alignItems: "center", justifyContent: "center" }} /> + } + />
+ {!window.isMobile && +
+ +
}
) } diff --git a/packages/app/src/layout/index.js b/packages/app/src/layout/index.js deleted file mode 100644 index fa8ed05d..00000000 --- a/packages/app/src/layout/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import Sidebar from './sidebar' -import Header from './header' -import Drawer from './drawer' -import Sidedrawer from './sidedrawer' - -export { - Drawer, - Sidebar, - Header, - Sidedrawer, -} \ No newline at end of file diff --git a/packages/app/src/layout/index.jsx b/packages/app/src/layout/index.jsx new file mode 100644 index 00000000..68f4b614 --- /dev/null +++ b/packages/app/src/layout/index.jsx @@ -0,0 +1,104 @@ +import React from "react" +import classnames from "classnames" +import * as antd from 'antd' +import { enquireScreen, unenquireScreen } from 'enquire-js' + +import Sidebar from './sidebar' +import Header from './header' +import Drawer from './drawer' +import Sidedrawer from './sidedrawer' +import BottomBar from "./bottombar" + +const LayoutRenders = { + mobile: (props) => { + return + + +
+ {props.children} +
+
+
+ + +
+ }, + default: (props) => { + return + + + +
+ +
+ {props.children} +
+
+ + + + } +} + +export default class Layout extends React.Component { + state = { + layoutType: "default", + isMobile: false, + isOnTransition: false, + } + + setLayout = (layout) => { + if (typeof LayoutRenders[layout] === "function") { + return this.setState({ + layoutType: layout, + }) + } + + return console.error("Layout type not found") + } + + componentDidMount() { + this.enquireHandler = enquireScreen(mobile => { + const { isMobile } = this.state + + if (isMobile !== mobile) { + window.isMobile = mobile + this.setState({ + isMobile: mobile, + }) + } + + if (mobile) { + window.app.eventBus.emit("mobile_mode") + this.setLayout("mobile") + } else { + window.app.eventBus.emit("desktop_mode") + this.setLayout("default") + } + }) + + window.app.eventBus.on("transitionStart", () => { + this.setState({ isOnTransition: true }) + }) + window.app.eventBus.on("transitionDone", () => { + this.setState({ isOnTransition: false }) + }) + } + + componentWillUnmount() { + unenquireScreen(this.enquireHandler) + } + + render() { + const layoutComponentProps = { + ...this.props, + ...this.state, + } + + if (LayoutRenders[this.state.layoutType]) { + return LayoutRenders[this.state.layoutType](layoutComponentProps) + } + + return LayoutRenders.default(layoutComponentProps) + } +} \ No newline at end of file diff --git a/packages/app/src/layout/sidebar/components/editor/index.jsx b/packages/app/src/layout/sidebar/components/editor/index.jsx index 70e5c49c..96492fef 100644 --- a/packages/app/src/layout/sidebar/components/editor/index.jsx +++ b/packages/app/src/layout/sidebar/components/editor/index.jsx @@ -6,8 +6,8 @@ import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd" import Selector from "../selector" +import sidebarItems from "schemas/routes.json" import defaultSidebarKeys from "schemas/defaultSidebar.json" -import sidebarItems from "schemas/sidebar.json" import "./index.less" diff --git a/packages/app/src/layout/sidebar/components/selector/index.jsx b/packages/app/src/layout/sidebar/components/selector/index.jsx index 4b608a40..8207c091 100644 --- a/packages/app/src/layout/sidebar/components/selector/index.jsx +++ b/packages/app/src/layout/sidebar/components/selector/index.jsx @@ -3,7 +3,7 @@ import { Icons, createIconRender } from "components/Icons" import { SelectableList } from "components" import { List } from "antd" -import sidebarItems from "schemas/sidebar.json" +import sidebarItems from "schemas/routes.json" import "./index.less" diff --git a/packages/app/src/layout/sidebar/index.jsx b/packages/app/src/layout/sidebar/index.jsx index 740e3f20..6eee53c7 100644 --- a/packages/app/src/layout/sidebar/index.jsx +++ b/packages/app/src/layout/sidebar/index.jsx @@ -1,11 +1,11 @@ import React from "react" -import { Icons, createIconRender } from "components/icons" +import { Icons, createIconRender } from "components/Icons" import { Layout, Menu, Avatar } from "antd" import { SidebarEditor } from "./components" import config from "config" -import sidebarItems from "schemas/sidebar.json" +import sidebarItems from "schemas/routes.json" import defaultSidebarItems from "schemas/defaultSidebar.json" import classnames from "classnames" diff --git a/packages/app/src/layout/sidebar/index.less b/packages/app/src/layout/sidebar/index.less index aea78a89..9a829b27 100644 --- a/packages/app/src/layout/sidebar/index.less +++ b/packages/app/src/layout/sidebar/index.less @@ -1,4 +1,4 @@ -@import "@/theme/vars.less"; +@import "theme/vars.less"; // SIDEBAR .ant-layout-sider { @@ -19,8 +19,7 @@ } } -.ant-menu, -ul { +.ant-menu, .ant-menu ul { background: transparent !important; background-color: transparent !important; diff --git a/packages/app/src/locales/en.js b/packages/app/src/locales/en.js deleted file mode 100644 index e3894890..00000000 --- a/packages/app/src/locales/en.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - Profile: "Profile", -} \ No newline at end of file diff --git a/packages/app/src/locales/es.js b/packages/app/src/locales/es.js deleted file mode 100644 index e20bf555..00000000 --- a/packages/app/src/locales/es.js +++ /dev/null @@ -1,12 +0,0 @@ -export default { - "Login": "Iniciar sesión", - "Back": "Volver", - "Profile": "Perfil", - "Settings": "Ajustes", - "Help & Assitence": "Ayuda y Asistencia", - "My Data": "Mis Datos", - "Statistics": "Estadistica", - "Workload": "Carga de Trabajo", - "Language": "Idioma", - welcome_index: "Bienvenido! {user}" -} \ No newline at end of file diff --git a/packages/app/src/models/session/index.js b/packages/app/src/models/session/index.js new file mode 100644 index 00000000..ddbb74b4 --- /dev/null +++ b/packages/app/src/models/session/index.js @@ -0,0 +1,135 @@ +import cookies from 'js-cookie' +import jwt_decode from "jwt-decode" +import config from 'config' + +export default class Session { + static get bridge() { + return window.app?.request + } + + static tokenKey = config.app?.storage?.token ?? "token" + + static get token() { + if (navigator.userAgent === "capacitor") { + // FIXME: sorry about that + return sessionStorage.getItem(this.tokenKey) + } + return cookies.get(this.tokenKey) + } + + static set token(token) { + if (navigator.userAgent === "capacitor") { + // FIXME: sorry about that + return sessionStorage.setItem(this.tokenKey, token) + } + return cookies.set(this.tokenKey, token) + } + + static get decodedToken() { + return this.token && jwt_decode(this.token) + } + + //* BASIC HANDLERS + login = (payload, callback) => { + const body = { + username: window.btoa(payload.username), + password: window.btoa(payload.password), + allowRegenerate: payload.allowRegenerate + } + + return this.generateNewToken(body, (err, res) => { + if (typeof callback === 'function') { + callback(err, res) + } + + if (!err || res.status === 200) { + let token = res.data + + if (typeof token === 'object') { + token = token.token + } + + Session.token = token + window.app.eventBus.emit("new_session") + } + }) + } + + logout = async () => { + await this.destroyCurrentSession() + this.forgetLocalSession() + } + + //* GENERATORS + generateNewToken = async (payload, callback) => { + const request = await Session.bridge.post.login(payload, undefined, { + parseData: false + }) + + if (typeof callback === 'function') { + callback(request.error, request.response) + } + + return request + } + + regenerateToken = async () => { + return await Session.bridge.post.regenerate() + } + + //* GETTERS + getAllSessions = async () => { + return await Session.bridge.get.sessions() + } + + getTokenInfo = async () => { + const session = Session.token + + return await Session.bridge.post.validateSession({ session }) + } + + isCurrentTokenValid = async () => { + const health = await this.getTokenInfo() + + return health.valid + } + + forgetLocalSession = () => { + if (navigator.userAgent === "capacitor") { + // FIXME: sorry about that + return sessionStorage.removeItem(Session.tokenKey) + } + return cookies.remove(Session.tokenKey) + } + + destroyAllSessions = async () => { + const session = Session.decodedToken + + if (!session) { + return false + } + + const result = await Session.bridge.delete.sessions({ user_id: session.user_id }) + this.forgetLocalSession() + window.app.eventBus.emit("destroyed_session") + + return result + } + + destroyCurrentSession = async () => { + const token = Session.token + const session = Session.decodedToken + + if (!session || !token) { + return false + } + + const result = await Session.bridge.delete.session({ user_id: session.user_id, token: token }) + this.forgetLocalSession() + window.app.eventBus.emit("destroyed_session") + + return result + } + + logout = this.destroyCurrentSession +} \ No newline at end of file diff --git a/packages/app/src/controllers/settings/index.js b/packages/app/src/models/settings/index.js similarity index 100% rename from packages/app/src/controllers/settings/index.js rename to packages/app/src/models/settings/index.js diff --git a/packages/app/src/controllers/sidebar/index.js b/packages/app/src/models/sidebar/index.js similarity index 100% rename from packages/app/src/controllers/sidebar/index.js rename to packages/app/src/models/sidebar/index.js diff --git a/packages/app/src/models/user/index.js b/packages/app/src/models/user/index.js index e923f8b8..d77cb319 100644 --- a/packages/app/src/models/user/index.js +++ b/packages/app/src/models/user/index.js @@ -25,6 +25,16 @@ export default class User { return User.bridge.get.roles({ username: token.username }) } + getAssignedWorkloads = async () => { + const token = Session.decodedToken + + if (!token || !User.bridge) { + return false + } + + return User.bridge.get.workloads({ username: token.username }) + } + getData = async (payload, callback) => { const request = await User.bridge.get.user(undefined, { username: payload.username, _id: payload.user_id }, { parseData: false diff --git a/packages/app/src/pages/account/components/sessionsView/index.jsx b/packages/app/src/pages/account/components/sessionsView/index.jsx index 8f3e6b20..841120ec 100644 --- a/packages/app/src/pages/account/components/sessionsView/index.jsx +++ b/packages/app/src/pages/account/components/sessionsView/index.jsx @@ -1,6 +1,5 @@ import React from "react" import * as antd from "antd" -import { Icons } from "components/Icons" import { Sessions } from "components" export default class SessionsView extends React.Component { @@ -9,8 +8,12 @@ export default class SessionsView extends React.Component { title: "Caution", content: "This action will cause all sessions to be closed, you will have to log in again.", onOk: () => { - //this.setState({ sessions: null }) - window.app.eventBus.emit("destroyAllSessions") + if (typeof this.props.handleSignOutAll === "function") { + this.props.handleSignOutAll() + } else { + antd.message.error("Sign out all sessions failed") + console.error("handleSignOutAll is not a function") + } }, okCancel: true, }) diff --git a/packages/app/src/pages/account/index.jsx b/packages/app/src/pages/account/index.jsx index 01052b84..71776ba6 100644 --- a/packages/app/src/pages/account/index.jsx +++ b/packages/app/src/pages/account/index.jsx @@ -8,7 +8,6 @@ import { Session } from "models" import "./index.less" -const api = window.app.apiBridge const SelfViewComponents = { sessionsView: SessionsView, @@ -68,6 +67,8 @@ export default class Account extends React.Component { sessions: null } + api = window.app.request + componentDidMount = async () => { const token = Session.decodedToken const location = window.app.history.location @@ -90,13 +91,14 @@ export default class Account extends React.Component { handleUpdateUserData = async (changes, callback) => { const update = {} + if (Array.isArray(changes)) { changes.forEach((change) => { update[change.id] = change.value }) } - await api.put + await this.api.put .selfUser(update) .then((data) => { callback(false, data) @@ -105,7 +107,11 @@ export default class Account extends React.Component { callback(true, err) }) - window.app.eventBus.emit("forceReloadUser") + window.app.eventBus.emit("reinitializeUser") + } + + handleSignOutAll = () => { + return this.props.contexts.app.sessionController.destroyAllSessions() } openUserEdit = () => { @@ -158,9 +164,6 @@ export default class Account extends React.Component { #{user._id} } -
-
-
{this.state.isSelf && this.renderSelfActions()}
@@ -173,10 +176,11 @@ export default class Account extends React.Component { sessions: this.state.sessions, user: this.state.user, decodedToken: Session.decodedToken, + handleSignOutAll: this.handleSignOutAll, }} /> )}
) } -} +} \ No newline at end of file diff --git a/packages/app/src/pages/explore/index.js b/packages/app/src/pages/explore/index.js deleted file mode 100644 index f7f6ed04..00000000 --- a/packages/app/src/pages/explore/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' -import { PostsFeed } from 'components' - -export default class Explore extends React.Component { - render() { - return - } -} diff --git a/packages/app/src/pages/explore/index.less b/packages/app/src/pages/explore/index.less deleted file mode 100644 index 8f4566f7..00000000 --- a/packages/app/src/pages/explore/index.less +++ /dev/null @@ -1,3 +0,0 @@ -.exploreWrapper{ - -} \ No newline at end of file diff --git a/packages/app/src/pages/main/index.jsx b/packages/app/src/pages/main/index.jsx index 8921f404..d3cc293b 100644 --- a/packages/app/src/pages/main/index.jsx +++ b/packages/app/src/pages/main/index.jsx @@ -1,18 +1,22 @@ import React from "react" -import { AppSearcher } from "components" +import * as antd from "antd" +import { AppSearcher, ServerStatus, Clock } from "components" import "./index.less" +// TODO: Customizable main menu export default class Main extends React.Component { - componentWillUnmount() { - if (!window.app?.HeaderController?.isVisible()) { - window.app.HeaderController.toogleVisible(true) + api = window.app.request + + componentDidMount() { + if (!window.isMobile && window.app?.HeaderController?.isVisible()) { + window.app.HeaderController.toogleVisible(false) } } - - componentDidMount() { - if (window.app?.HeaderController?.isVisible()) { - window.app.HeaderController.toogleVisible(false) + + componentWillUnmount() { + if (!window.isMobile && !window.app?.HeaderController?.isVisible()) { + window.app.HeaderController.toogleVisible(true) } } @@ -22,16 +26,27 @@ export default class Main extends React.Component { return (
-
-

Welcome back, {user.fullName ?? user.username ?? "Guest"}

+
+
+ +
+
+
+ +
+
+

Welcome back, {user.fullName ?? user.username ?? "Guest"}

+
+ {!window.isMobile &&
+ +
} +
-
+ {!window.isMobile &&
-
-
-
+
}
) } -} +} \ No newline at end of file diff --git a/packages/app/src/pages/main/index.less b/packages/app/src/pages/main/index.less index 5660d036..05b377a7 100644 --- a/packages/app/src/pages/main/index.less +++ b/packages/app/src/pages/main/index.less @@ -1,30 +1,52 @@ .dashboard { - padding-top: 20px; - width: 100%; + padding: 20px; + width: 100%; - h1{ - font-size: 28px; - margin: 0; - } + h1 { + font-size: 28px; + margin: 0; + } - .top { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - - > div { - margin-right: 20px; - } - } + .top { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; - > div { - margin-bottom: 20px; - } + > div { + margin-right: 20px; + } + } - .content { - > div { - margin-left: 20px; - } - } -} \ No newline at end of file + > div { + margin-bottom: 20px; + } + + .content { + > div { + margin-left: 20px; + } + } + + .assigned { + > div { + margin-left: 20px; + } + } +} + +.header_title { + display: flex; + > div { + margin-right: 20px; + } +} + +.quick_actions { + display: flex; + flex-wrap: wrap; + + > div { + margin: 6px 10px; + } +} diff --git a/packages/app/src/pages/streams/index.jsx b/packages/app/src/pages/streams/index.jsx new file mode 100644 index 00000000..37ec4f10 --- /dev/null +++ b/packages/app/src/pages/streams/index.jsx @@ -0,0 +1,54 @@ +import React from 'react' +import axios from "axios" +import * as antd from "antd" +import { SelectableList } from "components" + +// http://192.168.1.144:8080/live/srgooglo_ff.m3u8 +const streamsApi = "http://media.ragestudio.net/api" +const bridge = axios.create({ + baseURL: streamsApi, + auth: { + username: "admin", + password: "sharedpass1414" + } +}) + +export default class Streams extends React.Component { + state = { + list: {}, + } + + updateStreamsList = async () => { + const streams = ((await bridge.get("/streams")).data).live + this.setState({ list: streams }) + } + + componentDidMount = async () => { + this.updateStreamsList() + } + + onClickItem = (item) => { + window.app.setLocation(`/streams/viewer?key=${item}`) + } + + renderListItem = (key) => { + const streaming = this.state.list[key] + console.log(streaming) + return
this.onClickItem(key)}> +

@{streaming.publisher.stream} #{streaming.publisher.clientId}

+
+ } + + render() { + return
+

Streams

+
+ +
+
+ } +} \ No newline at end of file diff --git a/packages/app/src/pages/streams/viewer/index.jsx b/packages/app/src/pages/streams/viewer/index.jsx new file mode 100644 index 00000000..815b3c27 --- /dev/null +++ b/packages/app/src/pages/streams/viewer/index.jsx @@ -0,0 +1,99 @@ +import React from 'react' +import * as antd from "antd" +import Plyr from 'plyr' +import Hls from 'hls.js' +import mpegts from 'mpegts.js' + +import "plyr/dist/plyr.css" + +const streamsSource = "http://media.ragestudio.net/live" + +export default class StreamViewer extends React.Component { + state = { + player: null, + streamKey: null, + streamSource: null, + loadedProtocol: "flv", + protocolInstance: null, + defaultOptions: undefined, + } + + videoPlayerRef = React.createRef() + + componentDidMount = async () => { + const query = new URLSearchParams(window.location.search) + const requested = query.get("key") + + const source = `${streamsSource}/${requested}` + const player = new Plyr('#player') + + await this.setState({ + player, + streamKey: requested, + streamSource: source, + }) + + await this.loadWithProtocol[this.state.loadedProtocol]() + } + + updateQuality = (newQuality) => { + if (loadedProtocol === "hls") { + this.state.protocolInstance.levels.forEach((level, levelIndex) => { + if (level.height === newQuality) { + console.log("Found quality match with " + newQuality); + this.state.protocolInstance.currentLevel = levelIndex; + } + }) + } + else { + console.error("Unsupported protocol") + } + } + + switchProtocol = (protocol) => { + if (typeof this.state.protocolInstance.destroy === "function") { + this.state.protocolInstance.destroy() + } + + this.setState({ protocolInstance: null }) + + console.log("Switching to " + protocol) + this.loadWithProtocol[protocol]() + } + + loadWithProtocol = { + hls: () => { + const source = `${this.state.streamSource}.m3u8` + const hls = new Hls() + + hls.loadSource(source) + hls.attachMedia(this.videoPlayerRef.current) + + this.setState({ protocolInstance: hls, loadedProtocol: "hls" }) + }, + flv: () => { + const source = `${this.state.streamSource}.flv` + + const instance = mpegts.createPlayer({ type: 'flv', url: source, isLive: true }) + + instance.attachMediaElement(this.videoPlayerRef.current) + instance.load() + instance.play() + + this.setState({ protocolInstance: instance, loadedProtocol: "flv" }) + }, + } + + render() { + return
+ this.switchProtocol(value)} + value={this.state.loadedProtocol} + > + HLS + FLV + +
+ } +} \ No newline at end of file diff --git a/packages/app/src/pages/users/index.jsx b/packages/app/src/pages/users/index.jsx new file mode 100644 index 00000000..503ded1a --- /dev/null +++ b/packages/app/src/pages/users/index.jsx @@ -0,0 +1,93 @@ +import React from "react" +import * as antd from "antd" +import { Icons } from "components/icons" +import { ActionsBar, SelectableList } from "components" + +import "./index.less" + +export default class Users extends React.Component { + state = { + data: null, + selectionEnabled: false, + } + + api = window.app.request + + componentDidMount = async () => { + await this.loadData() + } + + loadData = async () => { + this.setState({ data: null }) + const data = await this.api.get.users() + this.setState({ data }) + } + + toogleSelection = (to) => { + this.setState({ selectionEnabled: to ?? !this.state.selectionEnabled }) + } + + openUser(username) { + if (this.state.selectionEnabled) { + return false + } + + window.app.setLocation(`/account`, { username }) + } + + renderRoles(roles) { + return roles.map((role) => { + return {role} + }) + } + + renderItem = (item) => { + return ( +
this.openUser(item.username)} + className="user_item" + > +
+ +
+
+
+
+

{item.fullName ?? item.username}

+
+
+

#{item._id}

+
+
+
{this.renderRoles(item.roles)}
+
+
+ ) + } + + render() { + return ( +
+
+ +
+ : } type={this.state.selectionEnabled ? "default" : "primary"} onClick={() => this.toogleSelection()}> + {this.state.selectionEnabled ? "Done" : "Select"} + +
+
+ }>New User +
+
+ {!this.state.data ? : + } +
+
+ ) + } +} diff --git a/packages/app/src/pages/users/index.less b/packages/app/src/pages/users/index.less new file mode 100644 index 00000000..86403e0b --- /dev/null +++ b/packages/app/src/pages/users/index.less @@ -0,0 +1,52 @@ +.users_list { + display: flex; + flex-direction: column; + + > div { + margin-bottom: 20px; + } + + .selectableList_item { + padding: 0; + } +} + +.user_item { + display: flex; + + > div { + margin: 7px; + } + + .title { + width: 100%; + display: flex; + + flex-direction: row; + align-items: center; + justify-content: space-between; + + h1 { + cursor: text; + margin: 0; + font-size: 16px; + user-select: all; + } + h3 { + font-family: "Roboto Mono", monospace !important; + cursor: text; + margin: 0; + font-size: 13px; + user-select: all; + } + + .line { + display: flex; + flex-direction: row; + align-items: center; + > div { + margin-right: 8px; + } + } + } +} \ No newline at end of file diff --git a/packages/app/src/theme/index.less b/packages/app/src/theme/index.less index 76a57860..2b4b6aee 100644 --- a/packages/app/src/theme/index.less +++ b/packages/app/src/theme/index.less @@ -3,12 +3,11 @@ @import "theme/fonts.css"; ::-webkit-scrollbar { - position: absolute; + display: none; - width: 14px; - height: 18px; - z-index: 200; - transition: all 200ms ease-in-out; + width: 0; + height: 0; + z-index: 0; } ::-webkit-scrollbar-thumb { @@ -111,6 +110,27 @@ body { overflow: hidden; transition: all 150ms ease-in-out; + + ::-webkit-scrollbar { + display: block; + position: absolute; + + width: 14px; + height: 18px; + z-index: 200; + transition: all 200ms ease-in-out; + } + + &.mobile { + padding-top: 20px; + + ::-webkit-scrollbar { + display: none !important; + width: 0; + height: 0; + z-index: 0; + } + } } .layout_page { @@ -125,25 +145,11 @@ body { overflow-y: overlay; } -// @media (min-width: @screen-xs) { - -// } - -// @media (min-width: @screen-md) { - -// } - -// @media (min-width: @screen-lg) { - -// } - -// @media (min-width: @screen-xl) { - -// } - -// @media (min-width: @screen-xxl) { - -// } +@media (max-width: 768px) { + .layout_page  { + margin: 10px; + } +} .fade-transverse-active { transition: all 250ms; @@ -208,8 +214,25 @@ body { justify-content: center; } -.ant-result-extra  { +.ant-result-extra { display: flex; align-items: center; justify-content: center; } + +.ant-modal-confirm-btns { + display: flex; +} + +.__render_box_test { + position: absolute; + z-index: 9999; + + width: 40vw; + height: 40vw; + + background: red; + transition: all 50ms ease-out; + + filter: drop-shadow(20px 20px 3px rgba(0, 0, 0, 1)); +} \ No newline at end of file diff --git a/packages/app/src/theme/vars.less b/packages/app/src/theme/vars.less index 19cce55e..8532cd78 100644 --- a/packages/app/src/theme/vars.less +++ b/packages/app/src/theme/vars.less @@ -4,7 +4,8 @@ @app_sidebar_borderRadius: 18px; // SIZES -@app_header_height: 55px; +@app_header_height: 5vh; +@fixedHeader100VH: @app_header_height - 100vh; @app_menuItemSize: 100px; @app_menuItemIconSize: 30px;