feat(CodePlayground): Visualization of component code in real time

This commit is contained in:
Jorge Mario Arita Ramírez 2024-01-04 22:43:54 -06:00
parent 36757bd163
commit 10e935d2e7
37 changed files with 1252 additions and 91 deletions

36
.vscode/settings.json vendored
View File

@ -11,25 +11,25 @@
"source.fixAll.eslint": "explicit"
},
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#0894ba",
"activityBar.background": "#0894ba",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#99077a",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#0894ba",
"statusBar.background": "#066d89",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#0894ba",
"statusBarItem.remoteBackground": "#066d89",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#066d89",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#066d8999",
"titleBar.inactiveForeground": "#e7e7e799"
"activityBar.activeBackground": "#fac8fb",
"activityBar.background": "#fac8fb",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#95920b",
"activityBarBadge.foreground": "#15202b",
"commandCenter.border": "#15202b99",
"sash.hoverBorder": "#fac8fb",
"statusBar.background": "#f598f8",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#f068f5",
"statusBarItem.remoteBackground": "#f598f8",
"statusBarItem.remoteForeground": "#15202b",
"titleBar.activeBackground": "#f598f8",
"titleBar.activeForeground": "#15202b",
"titleBar.inactiveBackground": "#f598f899",
"titleBar.inactiveForeground": "#15202b99"
},
"peacock.color": "#066d89",
"peacock.color": "#f598f8",
"cSpell.words": [
"orbis",
"Orbis"

39
Dockerfile Normal file
View File

@ -0,0 +1,39 @@
## Build image and name it 'custom-next'
# docker build -t custom-next .
## Run container and name it 'OrbisPlayground'. Webpage is localhost:3000
# docker run -it --rm -dp 3000:3000 --name OrbisPlayground custom-next
## Connect to container
# docker exec -it OrbisPlayground sh
## Stop docker container
# docker stop OrbisPlayground
## All together
# docker stop OrbisPlayground & docker image rm -f custom-next & docker build -t custom-next . && docker run -it --rm -dp 3000:3000 --name OrbisPlayground custom-next && docker exec -it OrbisPlayground sh
# Start Dockerfile
ARG VERSION=18.16.0-alpine3.17
ARG DIR=OrbisPlayground
FROM node:${VERSION} as builder
# redeclare ARG because ARG not in build environment
ARG DIR
WORKDIR /${DIR}
COPY . .
RUN apk update
RUN apk add git
RUN yarn
RUN NODE_ENV=production yarn build
FROM node:${VERSION} as runner
# redeclare ARG because ARG not in build environment
ARG DIR
WORKDIR /${DIR}
COPY --from=builder /${DIR}/public ./public
COPY --from=builder /${DIR}/.next/standalone .
COPY --from=builder /${DIR}/.next/static ./.next/static
EXPOSE 3000
ENTRYPOINT ["node", "server.js"]

View File

@ -1,12 +1,12 @@
{
"constants": {
"plataforma": "web",
"publicPath": "/orbistemplate",
"publicPath": "/orbisPlayground",
"urlServerImages" : "https://gt.via-asesores.com/smartoperation/orbisapi/dtsrv/dev/operation?apikey=NTAzYzZlOTItMDcwZC00Zjg4LTljODMtNzBkNGQ5YjZhZTso",
"urlWebApi": "https://gt.via-asesores.com/smartoperation/orbisapi/api/dev/operation?apikey=NTAzYzZlOTItMDcwZC00Zjg4LTljODMtNzBkNGQ5YjZhZTso",
"urlUploadApi": "https://gt.via-asesores.com/smartoperation/orbisapi/upload/dev/operation?apikey=NTAzYzZlOTItMDcwZC00Zjg4LTljODMtNzBkNGQ5YjZhZTso",
"appTitle": "OrbisTemplate",
"idApp": "orbistemplate",
"appTitle": "orbisPlayground",
"idApp": "orbisPlayground",
"publicKey":"-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA205Ag8sXnqc0XsPa4NiS tZSca+3afzgkMdpotsIOphZxketyBLs4QOKYsAHGw51R68fbx5oLmDCn7a4n4ZtT u39ksIQg1lwQ3y7pqfb9BbYZKhtYigL8URUVrsQ5EuZxk9BOHHez59gizNzM+Vp0 zlnOuJVZdVdp3d+d1z+oE3ejsdXLGFEjAblo8GNQxTgxOXJk2VQ+4yQX5QN+mEYS FQpJqP9z5Y+/SVXlD3e943XjuNOFZwSG2uVkW3tuKsvGBOA38xLKydY9hb5y0WdM E0/hnOvB6gfIOovSmdTonDF3224iGQJa8RXss3SN+6NeLnhJQYGBri6U4sa0lNR/ 5vip/VCzaHliYERTztT2NgW6WUZAEW05gjN6Qid2eB7lKs/ND3BQkDHUKqouNDO1 xookeBqSg7fT/l3D6D7QzJE5Jc+bdZUDrr2MeYXehzbGg8sUBXJZbOu6GUkDSM5Y C8r/SnZhhA0ancQZZW/t4TmFNiLiGrqNS4uJf4UHKKsmXHCKDKB/bdlp60lTl6YF ocGzW6tBPdDFD7S5UTPqg//ob6mvuPFJ0E6t8Le60P+UiZIdmINe9dX9darS0VNH +eCVLj1J7iQNyXrelD5sE7xhAvQ3+jp3Q4mXWVgOZi1Uh/+/iNXDxrAtzKipYAOg zuyH0DDtO3E4JSiv4qr8o+UCAwEAAQ== -----END PUBLIC KEY-----",
"maxMbUpload": 4,
"defaultLocale": "es"

View File

@ -1,12 +1,12 @@
{
"constants": {
"plataforma": "web",
"publicPath": "/orbistemplate",
"publicPath": "/orbisPlayground",
"urlServerImages" : "https://gt.via-asesores.com/smartoperation/orbisapi/dtsrv/dev/orbis?apikey=NTAzYzZlOTItMDcwZC00Zjg4LTljODMtNzBkNGQ5YjZhZTMw",
"urlWebApi": "https://gt.via-asesores.com/smartoperation/orbisapi/api/dev/orbis?apikey=NTAzYzZlOTItMDcwZC00Zjg4LTljODMtNzBkNGQ5YjZhZTMw",
"urlUploadApi": "https://gt.via-asesores.com/smartoperation/orbisapi/upload/dev/orbis?apikey=NTAzYzZlOTItMDcwZC00Zjg4LTljODMtNzBkNGQ5YjZhZTMw",
"appTitle": "OrbisTemplate",
"idApp": "orbistemplate",
"appTitle": "orbisPlayground",
"idApp": "orbisPlayground",
"publicKey":"-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA205Ag8sXnqc0XsPa4NiS tZSca+3afzgkMdpotsIOphZxketyBLs4QOKYsAHGw51R68fbx5oLmDCn7a4n4ZtT u39ksIQg1lwQ3y7pqfb9BbYZKhtYigL8URUVrsQ5EuZxk9BOHHez59gizNzM+Vp0 zlnOuJVZdVdp3d+d1z+oE3ejsdXLGFEjAblo8GNQxTgxOXJk2VQ+4yQX5QN+mEYS FQpJqP9z5Y+/SVXlD3e943XjuNOFZwSG2uVkW3tuKsvGBOA38xLKydY9hb5y0WdM E0/hnOvB6gfIOovSmdTonDF3224iGQJa8RXss3SN+6NeLnhJQYGBri6U4sa0lNR/ 5vip/VCzaHliYERTztT2NgW6WUZAEW05gjN6Qid2eB7lKs/ND3BQkDHUKqouNDO1 xookeBqSg7fT/l3D6D7QzJE5Jc+bdZUDrr2MeYXehzbGg8sUBXJZbOu6GUkDSM5Y C8r/SnZhhA0ancQZZW/t4TmFNiLiGrqNS4uJf4UHKKsmXHCKDKB/bdlp60lTl6YF ocGzW6tBPdDFD7S5UTPqg//ob6mvuPFJ0E6t8Le60P+UiZIdmINe9dX9darS0VNH +eCVLj1J7iQNyXrelD5sE7xhAvQ3+jp3Q4mXWVgOZi1Uh/+/iNXDxrAtzKipYAOg zuyH0DDtO3E4JSiv4qr8o+UCAwEAAQ== -----END PUBLIC KEY-----",
"maxMbUpload": 4,
"defaultLocale": "es"

View File

@ -1,12 +1,12 @@
{
"constants": {
"plataforma": "web",
"publicPath": "/orbistemplate",
"publicPath": "/orbisPlayground",
"urlServerImages" : "https://localhost:3000/smartoperation/orbisapi/dtsrv/dev/orbis?apikey=NTAzYzZlOTItMDcwZC00Zjg4LTljODMtNzBkNGQ5YjZhZTMw",
"urlWebApi": "https://localhost:3000/smartoperation/orbisapi/api/dev/orbis?apikey=NTAzYzZlOTItMDcwZC00Zjg4LTljODMtNzBkNGQ5YjZhZTMw",
"urlUploadApi": "https://localhost:3000/smartoperation/orbisapi/upload/dev/orbis?apikey=NTAzYzZlOTItMDcwZC00Zjg4LTljODMtNzBkNGQ5YjZhZTMw",
"appTitle": "OrbisTemplate",
"idApp": "orbistemplate",
"appTitle": "orbisPlayground",
"idApp": "orbisPlayground",
"publicKey":"-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA205Ag8sXnqc0XsPa4NiS tZSca+3afzgkMdpotsIOphZxketyBLs4QOKYsAHGw51R68fbx5oLmDCn7a4n4ZtT u39ksIQg1lwQ3y7pqfb9BbYZKhtYigL8URUVrsQ5EuZxk9BOHHez59gizNzM+Vp0 zlnOuJVZdVdp3d+d1z+oE3ejsdXLGFEjAblo8GNQxTgxOXJk2VQ+4yQX5QN+mEYS FQpJqP9z5Y+/SVXlD3e943XjuNOFZwSG2uVkW3tuKsvGBOA38xLKydY9hb5y0WdM E0/hnOvB6gfIOovSmdTonDF3224iGQJa8RXss3SN+6NeLnhJQYGBri6U4sa0lNR/ 5vip/VCzaHliYERTztT2NgW6WUZAEW05gjN6Qid2eB7lKs/ND3BQkDHUKqouNDO1 xookeBqSg7fT/l3D6D7QzJE5Jc+bdZUDrr2MeYXehzbGg8sUBXJZbOu6GUkDSM5Y C8r/SnZhhA0ancQZZW/t4TmFNiLiGrqNS4uJf4UHKKsmXHCKDKB/bdlp60lTl6YF ocGzW6tBPdDFD7S5UTPqg//ob6mvuPFJ0E6t8Le60P+UiZIdmINe9dX9darS0VNH +eCVLj1J7iQNyXrelD5sE7xhAvQ3+jp3Q4mXWVgOZi1Uh/+/iNXDxrAtzKipYAOg zuyH0DDtO3E4JSiv4qr8o+UCAwEAAQ== -----END PUBLIC KEY-----",
"maxMbUpload": 4,
"defaultLocale": "es"

View File

@ -1,12 +1,12 @@
{
"constants": {
"plataforma": "web",
"publicPath": "/orbistemplate",
"publicPath": "/orbisPlayground",
"urlServerImages" : "https://www.via-asesores.com/smartoperation/orbisapi/dtsrv/prod/orbis?apikey=MDJmNTEwOWUtNWY2ZC00OGJlLThjZGQtNWM4NmEyNmZmN2U5",
"urlWebApi": "https://www.via-asesores.com/smartoperation/orbisapi/api/prod/orbis?apikey=MDJmNTEwOWUtNWY2ZC00OGJlLThjZGQtNWM4NmEyNmZmN2U5",
"urlUploadApi": "https://www.via-asesores.com/smartoperation/orbisapi/upload/prod/orbis?apikey=MDJmNTEwOWUtNWY2ZC00OGJlLThjZGQtNWM4NmEyNmZmN2U5",
"appTitle": "OrbisTemplate",
"idApp": "orbistemplate",
"appTitle": "orbisPlayground",
"idApp": "orbisPlayground",
"publicKey":"-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA205Ag8sXnqc0XsPa4NiS tZSca+3afzgkMdpotsIOphZxketyBLs4QOKYsAHGw51R68fbx5oLmDCn7a4n4ZtT u39ksIQg1lwQ3y7pqfb9BbYZKhtYigL8URUVrsQ5EuZxk9BOHHez59gizNzM+Vp0 zlnOuJVZdVdp3d+d1z+oE3ejsdXLGFEjAblo8GNQxTgxOXJk2VQ+4yQX5QN+mEYS FQpJqP9z5Y+/SVXlD3e943XjuNOFZwSG2uVkW3tuKsvGBOA38xLKydY9hb5y0WdM E0/hnOvB6gfIOovSmdTonDF3224iGQJa8RXss3SN+6NeLnhJQYGBri6U4sa0lNR/ 5vip/VCzaHliYERTztT2NgW6WUZAEW05gjN6Qid2eB7lKs/ND3BQkDHUKqouNDO1 xookeBqSg7fT/l3D6D7QzJE5Jc+bdZUDrr2MeYXehzbGg8sUBXJZbOu6GUkDSM5Y C8r/SnZhhA0ancQZZW/t4TmFNiLiGrqNS4uJf4UHKKsmXHCKDKB/bdlp60lTl6YF ocGzW6tBPdDFD7S5UTPqg//ob6mvuPFJ0E6t8Le60P+UiZIdmINe9dX9darS0VNH +eCVLj1J7iQNyXrelD5sE7xhAvQ3+jp3Q4mXWVgOZi1Uh/+/iNXDxrAtzKipYAOg zuyH0DDtO3E4JSiv4qr8o+UCAwEAAQ== -----END PUBLIC KEY-----",
"maxMbUpload": 4,
"defaultLocale": "es"

10
jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"vComponents/*": ["./node_modules/vComponents/dist/components/*"],
"via-ui/*": ["./node_modules/via-ui/dist/components/*"]
}
}
}

View File

@ -1,7 +1,7 @@
{
"name": "next-template",
"name": "orbis-playground",
"version": "0.1.0",
"description": "My Next.js project",
"description": "Orbis Playground",
"repository": {
"type": "git",
"url": "https://git.via-asesores.com/jmaritar/next-template"
@ -25,7 +25,13 @@
"lint": "next lint"
},
"dependencies": {
"@babel/standalone": "^7.23.7",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@codesandbox/sandpack-react": "^2.10.0",
"@headlessui/react": "^1.7.17",
"@uiw/codemirror-theme-monokai": "^4.21.21",
"@uiw/react-codemirror": "^4.21.21",
"config": "^3.3.9",
"js-cookie": "^3.0.5",
"js-md5": "^0.8.3",
@ -36,10 +42,15 @@
"react-dom": "^18",
"react-dropzone-component": "^3.2.0",
"react-icons": "^4.12.0",
"react-live": "^4.1.5",
"react-live-runner": "^1.0.5",
"react-runner": "^1.0.3",
"react-simple-hook-store": "^0.0.6",
"react-toastify": "^9.1.3",
"rosetta": "^1.1.0",
"vComponents": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-components.git"
"v-functions": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-functions.git",
"vComponents": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-components.git#dev_ja",
"via-ui": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/via-ui.git#dev_ja"
},
"devDependencies": {
"@types/node": "^20",

View File

@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
autoprefixer: {}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,32 @@
import React, { useState } from 'react'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { monokai } from '@uiw/codemirror-theme-monokai'
import { useStore } from '@/hooks/useStore'
// import { oneDark } from '@codemirror/theme-one-dark'
const CodeEditor = ({ code, onChange }) => {
const [currentCode, setCurrentCode] = useState(code)
const [theme] = useStore(s => s.theme, a => a.setTheme)
const onCodeChange = (value, viewUpdate) => {
onChange(value)
setCurrentCode(value)
}
return (
<>
<CodeMirror
value={currentCode}
extensions={[javascript({ jsx: true })]}
theme={theme === 'dark' ? monokai : undefined}
className='shadow-neon max-h-[50vh] overflow-auto rounded-lg border'
// readOnly={true}
onChange={onCodeChange}
/>
</>
)
}
export default CodeEditor

View File

@ -0,0 +1,88 @@
import React, { useEffect, useState } from 'react'
import { Icons } from '../Icons'
import { LiveProvider, LiveError, LivePreview } from 'react-live'
import CodeEditor from '../CodeEditor'
import useKeyPress from '@/hooks/useKeyPress'
import { toast } from 'react-toastify'
import presets from '@/utils/globalPresets'
const CodePlayground = ({ code, onChange, scope }) => {
const [view, setView] = useState('preview')
const [currentCode, setCurrentCode] = useState(code)
const toggleView = () => {
setView(view === 'preview' ? 'code' : 'preview')
}
useKeyPress('Tab', toggleView, true)
const copyToClipboard = () => {
navigator.clipboard.writeText(code).then(() => {
toast.success('Código copiado al portapapeles', presets.toaster)
}).catch(err => {
console.error('Algo salió mal al copiar el código: ', err)
})
}
const handleCodeChange = (editor, data, value) => {
setCurrentCode(editor)
onChange(editor)
}
useEffect(() => {
setCurrentCode(code)
}, [code])
return (
<div className='flex-row'>
<div className='mb-3 flex justify-end'>
<div className="shadow-neon shadow-neon-sky flex space-x-1 rounded-lg bg-slate-200 dark:!bg-slate-900">
<button
onClick={() => setView('preview')}
className={`flex items-center rounded-md px-2 py-[0.4375rem] pr-3 text-sm font-semibold ${view === 'preview' ? 'bg-white shadow dark:!bg-[#333333]' : 'bg-slate-200 dark:!bg-transparent'}`}
>
<Icons.Eye className={`h-5 w-5 flex-none ${view === 'preview' ? 'stroke-sky-500 dark:stroke-sky-300' : 'stroke-slate-600 dark:stroke-slate-400'}`}/>
<span className={`ml-2 ${view === 'preview' ? 'text-sky-500 dark:text-sky-300' : 'text-slate-600 dark:text-slate-400'}`}>
Preview
</span>
</button>
<button
onClick={() => setView('code')}
className={`flex items-center rounded-md px-2 py-[0.4375rem] pr-3 text-sm font-semibold ${view === 'code' ? 'bg-white shadow dark:!bg-[#333333]' : 'bg-slate-200 dark:!bg-transparent'}`}
>
<Icons.Code className={`h-5 w-5 flex-none ${view === 'code' ? 'stroke-sky-500 dark:stroke-sky-300' : 'stroke-slate-600 dark:stroke-slate-400'}`}/>
<span className={`ml-2 ${view === 'code' ? 'text-sky-500 dark:text-sky-300' : 'text-slate-600 dark:text-slate-400'}`}>
Code
</span>
</button>
</div>
<div className='my-1 ml-6 mr-3 h-auto w-[2px] bg-slate-900/10 dark:bg-slate-900/20' />
<button
onClick={copyToClipboard}
className="shadow-neon shadow-neon-sky group relative flex h-9 w-9 items-center justify-center rounded-md bg-white text-slate-600 hover:text-slate-900 dark:!bg-[#333333] dark:!stroke-sky-300 dark:hover:!text-sky-100"
>
<Icons.Copy className="h-5 w-5 stroke-slate-400 transition group-hover:rotate-[-4deg] group-hover:stroke-slate-600 group-active:group-hover:stroke-sky-500 dark:group-hover:stroke-sky-300" />
</button>
</div>
<div>
<LiveProvider code={currentCode} scope={scope} noInline={true}>
{ view === 'preview' &&
<div className={`${view === 'preview' ? '' : 'hidden'}`}>
<LivePreview className='shadow-neon rounded-lg border p-2' />
<LiveError className="mt-2 rounded bg-red-500 text-white" />
</div>
}
<div className={`${view === 'code' ? '' : 'hidden'}`}>
<CodeEditor code={currentCode} onChange={handleCodeChange} />
</div>
</LiveProvider>
</div>
</div>
)
}
export default CodePlayground

View File

@ -0,0 +1,25 @@
import { BsSearch } from 'react-icons/bs'
import { IoEllipsisVertical, IoFilter } from 'react-icons/io5'
import { DocumentPlusIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/solid'
import { FaChevronLeft, FaChevronRight, FaPaperclip, FaUser } from 'react-icons/fa'
import { RiEyeLine } from 'react-icons/ri'
import { LuEye, LuCode2, LuClipboardList } from 'react-icons/lu'
export const Icons = {
// Common Icons
Search: BsSearch,
New: DocumentPlusIcon,
Edit: PencilSquareIcon,
View: IoFilter,
Pre: FaChevronLeft,
Next: FaChevronRight,
User: FaUser,
Details: EyeIcon,
Attachments: FaPaperclip,
Options: IoEllipsisVertical,
Eye: LuEye,
Code: LuCode2,
Copy: LuClipboardList
}

View File

@ -0,0 +1,41 @@
import React, { useEffect, useRef } from 'react'
import { transform } from '@babel/standalone'
const LivePreview = ({ code }) => {
const iframeRef = useRef(null)
useEffect(() => {
if (!iframeRef.current) return
// Aquí, asegúrate de que 'code' contenga solo el JSX, no una declaración de componente.
// Por ejemplo, debería verse así: "<div>Hello from JSX!</div>;"
const transformedCode = transform(`<>${code}</>`, {
presets: ['react']
}).code
const iframe = iframeRef.current
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document
iframeDoc.open()
iframeDoc.write(`
<!DOCTYPE html>
<html>
<head><title>Preview</title></head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script type="text/javascript">
${transformedCode}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(React.Fragment, null, ...React.Children.toArray(React.createElement(() => (${code}), {}))));
</script>
</body>
</html>
`)
iframeDoc.close()
}, [code])
return <iframe ref={iframeRef} style={{ width: '100%', height: '300px', border: 'none' }} />
}
export default LivePreview

View File

@ -90,7 +90,7 @@ const FormLogin = ({ onLogin }) => {
onChange={(e) => { validate(e) }}
/>
<button
className={'via-append-input'}
className={'via-append-input shadow-neon'}
onClick={(e) => { e.stopPropagation() }}
disabled
>
@ -115,7 +115,7 @@ const FormLogin = ({ onLogin }) => {
onChange={(e) => { validate(e) }}
/>
<button
className={'via-append-input'}
className={'via-append-input shadow-neon'}
onClick={(e) => { e.stopPropagation(); e.preventDefault(); setViewPassword(!viewPassword) }}
>
{ viewPassword === false && <EyeIcon className="h-5 w-5" /> }
@ -127,7 +127,11 @@ const FormLogin = ({ onLogin }) => {
<button
type="submit"
disabled={!isInputValid}
className={`${'via-button'} ${(isInputValid ? 'bg-cyan-900' : 'bg-red-400')} mx-0 w-full justify-center`}
className={`via-button shadow-neon
${(isInputValid
? '!bg-theme-app-500'
: '!bg-danger')}
mx-0 w-full justify-center`}
>
<KeyIcon className="h-5 w-5 pr-2" />
{ i18n.t('login.connect') }

View File

@ -0,0 +1,118 @@
import React, { useState, useEffect, useRef } from 'react'
export default function ResizableDiv ({ children, id, element = {}, disable = false, direction = 'col', min = 20, grow = false, onChange = () => {}, onResize = () => {}, className = '', style = {} }) {
const [isMounted, setIsMounted] = useState(false)
const [modelo, setModelo] = useState({ ...element })
// const [eventUpdate, setEventUpdate] = useState(0)
const [initialPosX, setInitialPosX] = useState(null)
const [initialSizeX, setInitialSizeX] = useState(null)
const [initialPosY, setInitialPosY] = useState(null)
const [initialSizeY, setInitialSizeY] = useState(null)
const divRef = useRef(null)
const initialX = (e) => {
if (divRef != null && divRef.current != null) {
const resizable = divRef.current // document.getElementById('Resizable');
setInitialPosX(e.clientX)
setInitialSizeX(resizable.offsetWidth)
}
}
const resizeX = (e) => {
// if (divRef != null && divRef.current != null) {
// let resizable = divRef.current // document.getElementById('Resizable');
// resizable.style.width = `${parseInt(initialSizeX) + parseInt(e.clientX - initialPosX)}px`
// }
const newWidth = parseInt(initialSizeX) + parseInt(e.clientX - initialPosX)
onChange('width', newWidth, modelo)
}
const initialY = (e) => {
if (divRef != null && divRef.current != null) {
const resizable = divRef.current // document.getElementById('Resizable');
setInitialPosY(e.clientY)
setInitialSizeY(resizable.offsetHeight)
}
}
const resizeY = (e) => {
// if (divRef != null && divRef.current != null) {
// let resizable = divRef.current // document.getElementById('Resizable');
// resizable.style.height = `${parseInt(initialSizeY) + parseInt(e.clientY - initialPosY)}px`
// }
const newHeight = parseInt(initialSizeY) + parseInt(e.clientY - initialPosY)
onChange('height', newHeight, modelo)
}
const applyResize = () => {
// console.log(' applyResize ')
if (divRef != null && divRef.current != null) {
const resizable = divRef.current // document.getElementById('Resizable');
if (!grow || grow === false) {
if (direction === 'col') {
resizable.style.height = `${modelo.height}px`
resizable.style.width = '100%'
}
if (direction === 'row') {
resizable.style.height = '100%'
resizable.style.width = `${modelo.width}px`
}
} else {
if (direction === 'col') {
resizable.style.height = '5rem'
resizable.style.width = '100%'
}
if (direction === 'row') {
resizable.style.height = '100%'
resizable.style.width = '5rem'
}
}
}
setTimeout(() => {
onResize()
}, 100)
}
useEffect(() => {
if (isMounted) {
applyResize()
}
}, [modelo, isMounted])
useEffect(() => {
if (isMounted) {
if (JSON.stringify({ ...element, _dz_background_image: null }) !== JSON.stringify({ ...modelo, _dz_background_image: null })) {
setModelo({ ...element })
}
}
}, [element])
useEffect(() => {
setIsMounted(true)
}, [])
return (
<div
ref={divRef}
id={id}
className={` relative flex items-center justify-center ${direction === 'col' ? 'w-full' : (grow ? ' w-4 ' : '')} ${direction === 'row' ? 'h-full' : (grow ? ' h-4 ' : '')} ${className}`}
style={grow ? { flexGrow: 1, ...style } : { ...style }}>
{children}
{ disable !== true && direction === 'col' &&
<div
className=' drag-resize-div-x bg-sky-300 '
draggable='true'
onDragStart={initialY}
onDragEnd={resizeY} />
}
{ disable !== true && direction === 'row' &&
<div
className=' drag-resize-div-y bg-sky-300 '
draggable='true'
onDragStart={initialX}
onDragEnd={resizeX} />
}
</div>
)
}

View File

@ -0,0 +1,468 @@
import React, { useEffect, useRef, useState } from 'react'
import { ChevronDownIcon, ArrowLeftOnRectangleIcon, MagnifyingGlassIcon, XMarkIcon, Bars3Icon } from '@heroicons/react/20/solid'
import * as iconsFc from 'react-icons/fc'
import * as iconsMd from 'react-icons/md'
import * as HeroIcons from '@heroicons/react/24/solid'
import mq from 'js-mq'
import PropTypes from 'prop-types'
Sidebar.propTypes = {
sidebarOpen: PropTypes.bool,
setSidebarOpen: PropTypes.func,
menu: PropTypes.array,
sidebarStyles: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
optionStyles: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
iconOptionStyles: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
suboptionStyles: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
iconSuboptionStyles: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
onClickLogout: PropTypes.func,
companyObj: PropTypes.object,
userObj: PropTypes.object,
setTitle: PropTypes.func,
i18n: PropTypes.object,
router: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
environment: PropTypes.object,
presets: PropTypes.object,
appTitleStyles: PropTypes.string,
userInfoStyles: PropTypes.string
}
// funcion que agrupa un vector de objetos por un campo y lo convierte en arbol
const nest = (items, idMenu = null, link = 'id_menu_padre') =>
items
.filter(item => item[link] === idMenu)
.map(item => ({
...item,
children:
nest(items, item.id_menu).length <= 0
? undefined
: nest(items, item.id_menu)
}))
const fontSizeClass = ' xs:text-md sm:text-md md:text-md lg:text-base xl:text-base '
// From https://reactjs.org/docs/hooks-state.html
export default function Sidebar ({ sidebarOpen, setSidebarOpen = () => {}, menu, sidebarStyles, optionStyles = fontSizeClass, iconOptionStyles, suboptionStyles = fontSizeClass, iconSuboptionStyles, onClickLogout, companyObj, userObj, setTitle, i18n = { t: () => { return 'txtNone' } }, router = [], environment = {}, presets, appTitleStyles = '', userInfoStyles = fontSizeClass, hideInputSearch = false }) {
const trigger = useRef(null)
const sidebar = useRef(null)
const [options, setOptions] = useState([])
const [isMobile, setIsMobile] = useState(false)
const [searchText, setSearchText] = useState('')
const getFontSize = (base) => {
if (base != null &&
(base.includes('text-xs') || base.includes('text-sm') || base.includes('text-base') || base.includes('text-md') || base.includes('text-lg') || base.includes('text-xl'))) {
return ''
}
return fontSizeClass
}
// close on click outside
useEffect(() => {
const clickHandler = ({ target }) => {
if (!sidebar.current || !trigger.current) return
if (!sidebarOpen || sidebar.current.contains(target) || trigger.current.contains(target)) return
setSidebarOpen(false)
}
document.addEventListener('click', clickHandler)
return () => document.removeEventListener('click', clickHandler)
})
// close if the esc key is pressed
useEffect(() => {
const keyHandler = ({ keyCode }) => {
if (!sidebarOpen || keyCode !== 27) return
setSidebarOpen(false)
}
document.addEventListener('keydown', keyHandler)
return () => document.removeEventListener('keydown', keyHandler)
})
useEffect(() => {
const menus = nest(menu)
// console.log('menu', menus)
setOptions(menus)
}, [menu])
useEffect(() => {
registrarBreckpoint()
}, [])
/* eslint-disable */
const registrarBreckpoint = () => {
if (typeof document !== undefined) {
try {
mq.register([
/* eslint-enable */
{ name: 'mobile', query: '(max-width: 767px)' },
{ name: 'desktop', query: '(min-width: 768px)' }
])
mq.on('mobile', (e) => {
setIsMobile(true)
})
mq.on('desktop', (e) => {
setIsMobile(false)
})
const arrayEstadoMq = mq.getState()
if (arrayEstadoMq.length && (arrayEstadoMq[0] === 'not-mobile' || arrayEstadoMq[0] === 'desktop')) {
setIsMobile(false)
} else {
setIsMobile(true)
}
} catch (e) {
console.error(`Error al registrar mq breackpoints - ${e.message}`)
}
}
}
const getSidebarClass = () => {
let resultCss = ''
if (isMobile === true) {
resultCss = `flex flex-col z-60 top-1/3 h-4/6 overflow-y-auto w-full shrink-0 p-4 rounded-md duration-[400ms] ease-in-out
${sidebarOpen ? 'translate-y-0 fixed' : ' transform translate-y-[100vh] fixed'} `
} else {
resultCss = `flex flex-col z-40 left-0 top-0 h-screen overflow-y-auto w-64 shrink-0 p-4 transition-all duration-200 ease-in-out
${sidebarOpen ? 'translate-x-0 relative' : '-translate-x-64 absolute'} `
}
return resultCss
}
const setSelected = (option) => {
const menus = options.slice()
const idx = menus.indexOf(option)
option.isSelected = option.isSelected ? !option.isSelected : true
menus.splice(idx, 1, option)
setOptions(menus)
if (option.path && option.path !== null) {
setTitle(option.title)
setSidebarOpen(false)
router.push(`${option.path}/${environment.getTime()}`)
}
}
const setSelectedSubOption = (option, suboption) => {
const menus = options.slice()
const idxOption = menus.indexOf(option)
const idxSubOption = option.children.indexOf(suboption)
suboption.isSelected = suboption.isSelected ? !suboption.isSelected : true
option.children.splice(idxSubOption, 1, suboption)
setTitle(suboption.title)
menus.splice(idxOption, 1, option)
setOptions(menus)
setSidebarOpen(false)
router.push(`${suboption.path}/${environment.getTime()}`)
}
/* eslint-disable */
const SubMenuOption = ({ option }) => {
if (option.children && option.children.length > 0 && option.isSelected === true) {
return (
<ul id='dropdown-example' className='ml-1 py-1 space-y-1'>
{option.children.map((suboption) => {
if (suboption.type === 'divisor') {
return (<li key={suboption.id_menu}><hr /></li>)
}
//Iconos subMenu
const getIcono = (item) => {
try {
const IconComponent = HeroIcons[item.icon]
if (IconComponent != null) {
return IconComponent
}
const IconComponentMd = iconsMd[item.icon]
if (IconComponentMd != null) {
return IconComponentMd
}
const IconComponentFC = iconsFc[item.icon]
if (IconComponentFC != null) {
return IconComponentFC
}
return null
} catch (e) {
return null
}
}
let Icono = null
if (suboption.icon) {
Icono = getIcono(suboption)
}
return (
<li key={suboption.id_menu} onClick={() => setSelectedSubOption(option, suboption)} title={suboption.title} className='cursor-pointer'>
<div
className={`flex items-center p-2 pl-3 w-full truncate overflow-hidden rounded-lg transition duration-75 group cursor-pointer ${Array.isArray(suboptionStyles) ? suboptionStyles.join(' ') : suboptionStyles} ${getFontSize(suboptionStyles)}`}
title={suboption.title}
>
{Icono && <Icono className={`h-7 w-7 text-white fill-current ${Array.isArray(iconSuboptionStyles) ? iconSuboptionStyles.join(' ') : iconSuboptionStyles}`} />}
<div className='inline-flex truncate text-ellipsis'>
{/* {suboption.title} */}
{suboption.i18n ? i18n.t(suboption.i18n) : suboption.title}
</div>
{suboption.badge && (
<span className='ml-1 inline-flex items-center justify-center rounded-full bg-red-600 px-2 py-1 text-xs font-bold leading-none text-red-100'>{suboption.badge}</span>
)}
</div>
</li>
)
})}
</ul>
)
}
return null
}
/* eslint-enable */
const MenuOption = ({ option }) => {
// Iconos Menu
const getIcono = (item) => {
try {
const IconComponent = HeroIcons[item.icon]
if (IconComponent != null) {
return IconComponent
}
const IconComponentMd = iconsMd[item.icon]
if (IconComponentMd != null) {
return IconComponentMd
}
const IconComponentFC = iconsFc[item.icon]
if (IconComponentFC != null) {
return IconComponentFC
}
return null
} catch (e) {
return null
}
}
let Icono = null
if (option.icon) {
Icono = getIcono(option)
}
if (option.type === 'titulo') {
return (
<>
<button
type='button'
className={`group flex w-full items-center rounded-lg p-2 transition duration-75 ${Array.isArray(optionStyles) ? optionStyles.join(' ') : optionStyles} ${getFontSize(optionStyles)}`}
aria-controls='dropdown-example' data-collapse-toggle='dropdown-example'
title={option.title}
>
<div className='ml-3 flex-1 truncate whitespace-nowrap text-left'>
{/* {option.title} */}
{option.i18n ? i18n.t(option.i18n) : option.title}
</div>
{option.badge && (
<span className='ml-1 inline-flex items-center justify-center rounded-full bg-red-600 px-2 py-1 text-xs font-bold leading-none text-red-100'>{option.badge}</span>
) }
{Icono && <Icono className={`h-7 w-7 fill-current text-white ${Array.isArray(iconOptionStyles) ? iconOptionStyles.join(' ') : iconOptionStyles}`} />}
</button>
</>
)
}
return (
<>
<button
type='button'
className={`group flex w-full items-center rounded-lg p-2 transition duration-75 ${Array.isArray(optionStyles) ? optionStyles.join(' ') : optionStyles} ${getFontSize(optionStyles)}`}
aria-controls='dropdown-example' data-collapse-toggle='dropdown-example'
title={option.title}
onClick={() => setSelected(option)}
>
{Icono && <Icono className={`h-7 w-7 fill-current text-white ${Array.isArray(iconOptionStyles) ? iconOptionStyles.join(' ') : iconOptionStyles}`} />}
<div className='ml-3 flex-1 truncate whitespace-nowrap text-left'>
{/* {option.title} */}
{option.i18n ? i18n.t(option.i18n) : option.title}
</div>
{option.badge && (
<span className='ml-1 inline-flex items-center justify-center rounded-full bg-red-600 px-2 py-1 text-xs font-bold leading-none text-red-100'>{option.badge}</span>
) }
{option.children && option.children.length > 0 && <ChevronDownIcon className='h-6 w-6' />}
</button>
<SubMenuOption option={option} />
</>
)
}
MenuOption.propTypes = {
option: PropTypes.object
}
const getFilteredOptions = (options, searchText, originalMenu) => {
const searchLower = searchText.toLowerCase()
const cloneWithIsSelected = (items, selectedItems) => {
return items.map(item => {
const selectedItem = selectedItems.find(si => si.id_menu === item.id_menu)
const isSelected = selectedItem ? selectedItem.isSelected : false
const children = item.children ? cloneWithIsSelected(item.children, selectedItems) : []
return { ...item, isSelected, children }
})
}
const searchAndMergeItems = (items, isChildMatch = false) => {
return items.reduce((acc, item) => {
const matchSelf = item.title.toLowerCase().includes(searchLower)
const children = item.children ? searchAndMergeItems(item.children, matchSelf || isChildMatch) : []
const newItem = { ...item, children }
if (matchSelf || children.length > 0 || isChildMatch) {
const existingIndex = acc.findIndex(e => e.id_menu === item.id_menu)
if (existingIndex > -1) {
acc[existingIndex] = { ...acc[existingIndex], isSelected: newItem.isSelected || acc[existingIndex].isSelected }
} else {
acc.push(newItem)
}
}
return acc
}, [])
}
if (searchText) {
return searchAndMergeItems(options)
} else {
const selectedItems = options.filter(item => item.isSelected)
return cloneWithIsSelected(originalMenu, selectedItems)
}
}
const originalMenu = nest(menu)
const filteredOptions = getFilteredOptions(options, searchText, originalMenu)
return (
<>
{/* Sidebar backdrop (mobile only) bg-slate-900 */}
<div
className={`fixed inset-0 bg-opacity-30 z-40 md:hidden md:z-auto transition-opacity duration-200
${sidebarOpen ? 'opacity-100' : 'pointer-events-none opacity-0'} ${Array.isArray(sidebarStyles) ? sidebarStyles.join(' ') : sidebarStyles}`}
aria-hidden='true'
onClick={() => { setSidebarOpen(!sidebarOpen) }}
/>
{/* Sidebar */}
{/* lg:translate-x-0 lg:w-20 lg:static lg:left-auto lg:top-auto lg:overflow-y-auto lg:sidebar-expanded:!w-64 2xl:!w-64 */}
<div
id='sidebar'
ref={sidebar}
className={` ${getSidebarClass()} ${Array.isArray(sidebarStyles) ? sidebarStyles.join(' ') : sidebarStyles}`}
>
{/* Expand / collapse button */}
<div className='fixed right-6 top-4 justify-end pt-0 md:hidden '>
<div className='px-3 py-2'>
<button onClick={() => setSidebarOpen(!sidebarOpen)}>
<div className='sr-only'>Expand / collapse sidebar</div>
<XMarkIcon className={`text-${presets.theme}-600 h-6 w-6 rounded-md bg-white/50`} />
</button>
</div>
</div>
{/* Sidebar header */}
<div className='mb-10 flex justify-between pr-3 sm:px-2'>
{/* Close button */}
{/* <button
ref={trigger}
className={`absolute cursor-pointer -right-3 top-9 w-7 border-dark-purple z-50
border-2 rounded-full ${!sidebarOpen && 'rotate-180'}`}
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-controls='sidebar'
aria-expanded={sidebarOpen}
>
<div className='sr-only'>Close sidebar</div>
{sidebarOpen
? <ArrowLeftIcon className='w-6 h-6 fill-current text-white' />
: <ArrowRightIcon className='w-6 h-6 fill-current text-white' />
}
</button> */}
{/* Logo */}
<div className='flex items-center gap-x-2' title='Versión 3.0.10'>
<img
src={presets.images.icon}
className={`h-12 w-12 cursor-pointer duration-500 ${
sidebarOpen && 'rotate-[360deg]'
}`}
alt={presets.appTitle}
width={100}
height={100}
/>
<h1 className={`origin-left text-xl font-medium text-white duration-200 ${!sidebarOpen && 'scale-0'} ${appTitleStyles} `}>
{presets.appTitle}
</h1>
</div>
</div>
{/* user Info */}
<div className={` flex w-full flex-col space-y-0 border-b-2 border-gray-300 text-gray-50 ${userInfoStyles} ${getFontSize(userInfoStyles)}`}>
<div className='px-2 font-medium '> {userObj ? userObj.compania : ''} </div>
<div className='px-2'>{userObj ? userObj.nombre_usuario : ''}</div>
<div className='break-all px-2 '>{userObj ? userObj.email : ''}</div>
</div>
{/* Links */}
<div className={`my-0 overflow-y-auto rounded px-2 py-4 ${Array.isArray(sidebarStyles) ? sidebarStyles.join(' ') : sidebarStyles}`}>
{/* Search */}
{ hideInputSearch !== true &&
<div className='relative -mt-2 mb-1 flex h-9 justify-between'>
<input
id='searchText'
name='searchText'
value={searchText}
type='search'
className='via-input-search-sidebar focus:!outline-none focus:!ring-0'
onInput={(e) => { setSearchText(e.target.value) }}
onChange={(e) => { setSearchText(e.target.value) }}
/>
<button
className='via-button-search-sidebar'
onClick={(e) => { e.stopPropagation() }}
disabled
>
<MagnifyingGlassIcon className='h-5 w-5' />
</button>
</div>
}
<ul className='space-y-2'>
{filteredOptions.map((option) => {
/* eslint-disable */
return (
<li key={option.id_menu + '_li'}>
{option.type === 'divisor' && <hr key={option.id_menu} />}
{option.type !== 'divisor' && <MenuOption option={option} key={option.id_menu} />}
</li>
)
/* eslint-enable */
})}
<hr />
<li>
<button
type='button'
className={`group flex w-full items-center rounded-lg p-2 transition duration-75 ${Array.isArray(optionStyles) ? optionStyles.join(' ') : optionStyles} ${getFontSize(optionStyles)}`}
aria-controls='dropdown-example' data-collapse-toggle='dropdown-example'
onClick={() => onClickLogout()}
>
<ArrowLeftOnRectangleIcon className='h-6 w-6 pr-2' />
<div className='ml-3 flex-1 whitespace-nowrap text-left'>{i18n.t('common.logout')}</div>
</button>
</li>
</ul>
</div>
</div>
{ isMobile === true &&
<div className="fixed bottom-3 right-3 z-60" v-if="isMobile" >
<button
id="vtDrawerButton"
className={`bg-${presets.theme}-600 hover:bg-${presets.theme}-700 focus:shadow-outline-purple flex items-center justify-between rounded-lg border border-transparent p-2 font-normal leading-5 text-white transition-colors duration-150 focus:outline-none active:bg-purple-600`}
aria-label="Like"
onClick={() => { setSidebarOpen(!sidebarOpen) }}
>
<Bars3Icon className='h-6 w-6' />
</button>
</div>
}
</>
)
}

View File

@ -6,17 +6,17 @@ import { TbColorPicker, TbLanguage } from 'react-icons/tb'
const UserOptionsMenu = ({ setPreferences, theme, i18n }) => {
return (
<>
<div className="flex items-center justify-center text-gray-900">
<div className="flex items-center justify-center text-gray-900 dark:text-white">
<TbColorPicker className="mr-2 h-5 w-5" aria-hidden="true" />
Tema
</div>
<div className="mb-4 grid grid-cols-2 gap-2 p-1">
<div className="mb-2 grid grid-cols-2 gap-2 p-1">
<Menu.Item>
<button
className={`${
theme === 'light'
? 'bg-slate-700 text-white dark:bg-slate-800'
: 'bg-gray-200 text-gray-900 transition-colors duration-[350ms] ease-in-out hover:bg-gray-300'
? 'bg-theme-app-500 text-white dark:bg-theme-app-500'
: 'bg-theme-app-50 text-gray-900 transition-colors duration-[350ms] ease-in-out hover:bg-theme-app-100'
} group flex w-full items-center rounded-md p-2 text-sm`}
onClick={async () => {
setPreferences('light')
@ -30,8 +30,8 @@ const UserOptionsMenu = ({ setPreferences, theme, i18n }) => {
<button
className={`${
theme === 'dark'
? 'bg-slate-700 text-white dark:bg-slate-800'
: 'bg-gray-200 text-gray-900 transition-colors duration-[350ms] ease-in-out hover:bg-gray-300'
? 'bg-theme-app-500 text-white dark:bg-theme-app-500'
: 'bg-theme-app-50 text-gray-900 transition-colors duration-[350ms] ease-in-out hover:bg-theme-app-100'
} group flex w-full items-center rounded-md p-2 text-sm`}
onClick={() => {
setPreferences('dark')
@ -43,18 +43,18 @@ const UserOptionsMenu = ({ setPreferences, theme, i18n }) => {
</Menu.Item>
</div>
<div className="flex items-center justify-center text-gray-900">
<div className="flex items-center justify-center text-gray-900 dark:text-white">
<TbLanguage className="mr-2 h-5 w-5" aria-hidden="true" />
Idioma
</div>
<div className="grid grid-cols-2 gap-2 p-1">
<div className="mb-2 grid grid-cols-2 gap-2 p-1">
<Menu.Item>
<button
className={`${
i18n.activeLocale === 'es'
? 'bg-slate-700 text-white dark:bg-slate-800'
: 'bg-gray-200 text-gray-900 transition-colors duration-[350ms] ease-in-out hover:bg-gray-300'
? 'bg-theme-app-500 text-white dark:bg-theme-app-500'
: 'bg-theme-app-50 text-gray-900 transition-colors duration-[350ms] ease-in-out hover:bg-theme-app-100'
} group flex w-full items-center rounded-md p-2 text-sm`}
onClick={async () => {
setPreferences('es')
@ -82,8 +82,8 @@ const UserOptionsMenu = ({ setPreferences, theme, i18n }) => {
<button
className={`${
i18n.activeLocale === 'en'
? 'bg-slate-700 text-white dark:bg-slate-800'
: 'bg-gray-200 text-gray-900 transition-colors duration-[350ms] ease-in-out hover:bg-gray-300'
? 'bg-theme-app-500 text-white dark:bg-theme-app-500'
: 'bg-theme-app-50 text-gray-900 transition-colors duration-[350ms] ease-in-out hover:bg-theme-app-100'
} group flex w-full items-center rounded-md p-2 text-sm`}
onClick={() => {
setPreferences('en')

72
src/data/menu.json Normal file
View File

@ -0,0 +1,72 @@
[
{
"id_menu": "v-components",
"title": "v-components",
"descripcion": null,
"icon": "FcDeployment",
"path": null,
"orden": 1,
"id_menu_padre": null,
"badge": null
},
{
"id_menu": "data-table",
"title": "DataTable",
"descripcion": null,
"icon": "FcDataSheet",
"path": "/seguridad/compania",
"orden": 1.1,
"id_menu_padre": "v-components",
"badge": null
},
{
"id_menu": "data-form",
"title": "DataFrom",
"descripcion": null,
"icon": "FcKindle",
"path": "/seguridad/departamento",
"orden": 1.2,
"id_menu_padre": "v-components",
"badge": null
},
{
"id_menu": "via-ui",
"title": "via-ui",
"descripcion": null,
"icon": "FcCloseUpMode",
"path": "",
"orden": 2,
"id_menu_padre": null,
"badge": "New"
},
{
"id_menu": "tooltip",
"title": "Tooltip",
"descripcion": null,
"icon": "FcMms",
"path": "/via-ui/tooltip",
"orden": 2.1,
"id_menu_padre": "via-ui",
"badge": null
},
{
"id_menu": "tab",
"title": "Tab",
"descripcion": null,
"icon": "FcTemplate",
"path": "/via-ui/tab",
"orden": 2.1,
"id_menu_padre": "via-ui",
"badge": null
},
{
"id_menu": "test",
"title": "Test",
"descripcion": null,
"icon": "FcIdea",
"path": "/via-ui/test",
"orden": 2.1,
"id_menu_padre": "via-ui",
"badge": null
}
]

29
src/hooks/useKeyPress.js Normal file
View File

@ -0,0 +1,29 @@
import { useEffect } from 'react'
const useKeyPress = (targetKeys, onKeyPress, preventDefault = false) => {
useEffect(() => {
const handleKeyPress = (event) => {
const keys = Array.isArray(targetKeys) ? targetKeys : [targetKeys]
if (keys.every(key => {
if (key === 'Meta' || key === 'Control' || key === 'Alt' || key === 'Shift') {
return event.getModifierState(key)
} else {
return event.key === key
}
})) {
if (preventDefault) {
event.preventDefault()
}
onKeyPress()
}
}
window.addEventListener('keydown', handleKeyPress)
return () => {
window.removeEventListener('keydown', handleKeyPress)
}
}, [targetKeys, onKeyPress, preventDefault])
}
export default useKeyPress

View File

@ -2,12 +2,14 @@ import { createStore } from 'react-simple-hook-store'
interface IState {
title: string;
theme: string;
usuarioPermisos: object | null;
valueEasyMDE: string;
}
interface IActions {
setTitle: (newState: string) => void;
setTheme: (newState: string) => void;
setUsuarioPermisos: (newState: object | null) => void;
setValueForEasyMDE: (newState: string) => void;
}
@ -15,6 +17,7 @@ interface IActions {
export const { useStore, store } = createStore<IState, IActions>(
{
title: '',
theme: 'light',
usuarioPermisos: null,
valueEasyMDE: ''
},
@ -24,6 +27,11 @@ export const { useStore, store } = createStore<IState, IActions>(
title: newState
})
},
setTheme: (store, newState) => {
store.setState({
theme: newState
})
},
setUsuarioPermisos: (store, newState) => {
store.setState({
usuarioPermisos: newState

View File

@ -12,10 +12,12 @@ import SessionTimeout from '@/components/SessionTimeout'
import UserOptionsMenu from '@/components/widgets/UserOptionsMenu'
import { toast } from 'react-toastify'
import menuData from '@/data/menu.json'
export const LayoutContext = createContext()
const Footer = dynamic(() => { return import('vComponents/Footer') }, { ssr: false })
const Sidebar = dynamic(() => { return import('vComponents/Sidebar') }, { ssr: false })
const Sidebar = dynamic(() => { return import('@/components/vComponents/Sidebar') }, { ssr: false })
const Navbar = dynamic(() => { return import('vComponents/Navbar') }, { ssr: false })
const ResponsiveContainer = ({ children }) => {
@ -26,13 +28,14 @@ const ResponsiveContainer = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(true)
const [menu, setMenu] = useState([])
const [title, setTitle] = useStore(s => s.title, a => a.setTitle)
const [userObj, setUserObj] = useState()
const [token, setToken] = useState('')
const [theme, setTheme] = useState('light')
const [constanteObj, setConstanteObj] = useState('')
const [appVersion, setAppVersion] = useState('1.0.0')
const [title, setTitle] = useStore(s => s.title, a => a.setTitle)
const [theme, setTheme] = useStore(s => s.theme, a => a.setTheme)
const doLogout = async () => {
const redirectPath = await environment.logout()
await i18n.locale('es')
@ -107,7 +110,8 @@ const ResponsiveContainer = ({ children }) => {
loading.start()
setMenu([])
if (token && token !== null) {
const options = await execute('SPR_MENU_S', [token, 'BO', null])
// const options = await execute('SPR_MENU_S', [token, 'BO', null])
const options = menuData
setMenu(options)
}
loading.stop()
@ -232,6 +236,7 @@ const ResponsiveContainer = ({ children }) => {
userMenuButtonStyles="via-user-menu-btn-navbar"
userOptionStyles="via-user-options-navbar"
userOptionSelectedStyles="via-user-options-selected-navbar"
menuStyles="via-menu-navbar"
onClickLogout={() => doLogout()}
onClickProfile={() => router.push(`${presets.locations.profile}/${environment.getTime()}`)}
MenuOptions={() => <UserOptionsMenu setPreferences={setPreferences} theme={theme} i18n={i18n} />}
@ -243,7 +248,7 @@ const ResponsiveContainer = ({ children }) => {
}
<main>
<div className={userObj && userObj.nombre_usuario ? 'responsive-container' : 'hidden'}>
<div className={userObj && userObj.nombre_usuario ? 'responsive-container' : ''}>
{children}
</div>
</main>
@ -252,7 +257,7 @@ const ResponsiveContainer = ({ children }) => {
<SessionTimeout i18n={i18n} user={userObj} onTimeout={() => onTimeout()} onCancelTimeout={() => onCancelTimeout()} />
}
</div>
<Footer version={appVersion} label={i18n.t('common.version') || 'version'} />
<Footer footerStyle='via-footer' version={appVersion} label={i18n.t('common.version') || 'version'} />
</div>
</LayoutContext.Provider>
)

View File

@ -1,27 +1,34 @@
import { RouteProvider } from '@/hooks/useRoute'
import { useStore } from '@/hooks/useStore'
import ResponsiveContainer from '@/layout/ResponsiveContainer'
import LoadingProvider from '@/plugins/LoadingContext'
import I18nProvider from '@/plugins/i18nContext'
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import 'vComponents/styles/generated/output.css'
import 'vComponents/styles/generated/bgColors.min.css'
import { ToastContainer } from 'react-toastify'
export default function App ({ Component, pageProps }: AppProps) {
import '@/styles/globals.css'
import 'vComponents/styles/generated/bgColors.min.css'
import 'vComponents/styles/generated/output.css'
export default function App ({ Component, pageProps }) {
const [theme] = useStore(s => s.theme, a => a.setTheme)
return (
<>
<Head>
<link rel="icon" href="https://wiki.via-asesores.com/logo-orbis_min.png" type="image/x-icon" />
<title>Orbis Template</title>
<title>Orbis Playground</title>
</Head>
<I18nProvider locale={'es'} dict={''}>
<LoadingProvider>
<ToastContainer />
<ResponsiveContainer>
<ToastContainer
theme={theme === 'dark' ? 'dark' : 'light'}
toastClassName={theme === 'dark'
? 'bg-black shadow-neon text-white rounded-lg'
: 'bg-white text-black drop-shadow-lg rounded-lg shadow-lg'}
/>
<RouteProvider>
<Component {...pageProps} />
</RouteProvider>

View File

@ -0,0 +1,40 @@
import presets from '@/utils/globalPresets'
import React from 'react'
import { toast } from 'react-toastify'
const DashboardPage = () => {
return (
<>
<div className="grid grid-cols-4 gap-4 text-white">
<div className='bg-theme-app-50' >01</div>
<div className='bg-theme-app-100' >02</div>
<div className='bg-theme-app-200' >03</div>
<div className='bg-theme-app-300' >04</div>
<div className='bg-theme-app-400' >05</div>
<div className="col-span-3 grid grid-cols-subgrid gap-4 bg-theme-app-500">
<div className="col-start-2">06</div>
</div>
<div className='bg-theme-app-600' >07</div>
<div className='bg-theme-app-700' >08</div>
<div className='bg-theme-app-800' >09</div>
<div className='bg-theme-app-900' >10</div>
<div className='bg-theme-app-950' >11</div>
</div>
<button
className="via-button shadow-neon bg-theme-app-500 backdrop:bg-gray-50"
onClick={() => toast.success('Hola mundo', presets.toaster)}
>
Mostrar toast
</button>
<button className="shadow-neon shadow-neon-sky h-8 w-44 rounded-full bg-red-600 text-center text-purple-200" >
Button Neon
</button>
</>
)
}
export default DashboardPage

View File

@ -56,12 +56,12 @@ const LoginPage = () => {
max_mb_upload: process.env.maxMbUpload
}
await environment.login(user.token, user, constante)
toast.success(`Bienvenido ${user.nombre_usuario}`, presets.toaster)
await global.setTheme(user.theme)
await global.setUserObj(user)
await global.setConstanteObj(constante)
await global.setToken(resultado[0].token)
await global.setTheme(user.theme)
await i18n.locale(user.i18n)
toast.success(`Bienvenido ${user.nombre_usuario}`, presets.toaster)
router.push(homePath)
}
} catch (error) {

View File

@ -0,0 +1,9 @@
import React from 'react'
const viaUIPage = () => {
return (
<div>viaUIPage</div>
)
}
export default viaUIPage

View File

@ -0,0 +1,29 @@
import React from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
const TabPage = () => {
const [tab, setTab] = React.useState('data')
return (
<Tabs activeValue={tab}>
<TabsList>
<TabsTrigger value="data" onClick={() => setTab('data')}>
Datos del Evento
</TabsTrigger>
<TabsTrigger value="docs" onClick={() => setTab('docs')}>
Documentos del Evento
</TabsTrigger>
</TabsList>
<TabsContent value="data">
tabs data
</TabsContent>
<TabsContent value="docs">
tabs docs
</TabsContent>
</Tabs>
)
}
export default TabPage

View File

@ -0,0 +1,56 @@
import CodePlayground from '@/components/CodePlayground'
import React, { useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
const initialCode =
`// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
const TabsExample = () => {
const [tab, setTab] = React.useState('data');
return (
<>
<Tabs activeValue={tab} onChange={setTab}>
<TabsList>
<TabsTrigger value="data">Datos del Evento</TabsTrigger>
<TabsTrigger value="docs">Documentos del Evento</TabsTrigger>
</TabsList>
<TabsContent value="data">
tabs data
</TabsContent>
<TabsContent value="docs">
tabs docs
</TabsContent>
</Tabs>
</>
);
};
render(<TabsExample />);
`
const ExampleComponentPage = () => {
const [code, setCode] = useState(initialCode)
// El objeto scope debería incluir todos los componentes y funciones que el código JSX necesita
const scope = {
React,
useState,
Tabs,
TabsContent,
TabsList,
TabsTrigger
}
return (
<>
<div className='rounded-lg bg-white p-4 shadow-lg dark:!bg-[#222222]'>
<CodePlayground code={code} onChange={(newCode) => { setCode(newCode) }} scope={scope} />
</div>
</>
)
}
export default ExampleComponentPage

View File

@ -1,9 +1,9 @@
.via-navbar {
@apply bg-white dark:bg-[#222222] border-slate-200 text-theme-app-500 dark:text-theme-app-50 font-semibold !important;
@apply bg-white dark:bg-[#222222] border-slate-200 text-theme-app-500 dark:text-theme-app-50 font-semibold transition-colors duration-[350ms] ease-in-out !important;
}
.via-menu-btn-navbar {
@apply text-slate-600 hover:text-slate-900 !important;
@apply text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200 transition-colors duration-[350ms] ease-in-out shadow-neon rounded-md !important;
}
.via-user-menu-btn-navbar {
@ -11,9 +11,17 @@
}
.via-user-options-navbar {
@apply text-gray-900 bg-transparent transition-colors duration-[350ms] ease-in-out !important;
@apply bg-transparent border border-transparent dark:border-theme-app-400 text-gray-900 dark:text-white transition-colors duration-[350ms] ease-in-out !important;
}
.via-user-options-selected-navbar {
@apply bg-slate-700 dark:bg-slate-800 text-white transition-colors duration-[350ms] ease-in-out !important;
@apply bg-theme-app-50 border border-transparent dark:bg-theme-app-500 transition-colors duration-[350ms] ease-in-out !important;
}
.via-menu-navbar {
@apply bg-white dark:bg-[#222222] border-slate-200 text-black dark:text-white transition-colors duration-[350ms] ease-in-out !important;
}
.via-footer {
@apply bg-white dark:bg-[#222222] border-slate-200 text-black dark:text-white transition-colors duration-[350ms] ease-in-out text-xs !h-8 !important;
}

View File

@ -3,11 +3,11 @@
}
.via-options-sidebar {
@apply text-theme-app-50 bg-transparent transition-colors duration-150 font-semibold hover:bg-theme-app-50 hover:text-gray-800 dark:text-theme-app-50 dark:hover:bg-theme-app-700 first-letter:uppercase !important;
@apply text-theme-app-50 bg-transparent transition-colors duration-150 font-semibold hover:bg-theme-app-50 hover:text-gray-800 dark:text-theme-app-50 dark:hover:bg-theme-app-200 first-letter:uppercase !important;
}
.via-sub-options-sidebar {
@apply text-white hover:bg-theme-app-100 transition-colors duration-150 font-semibold gap-x-1 hover:text-gray-800 dark:text-theme-app-50 dark:hover:bg-theme-app-800 !important;
@apply text-white hover:bg-theme-app-100 transition-colors duration-150 font-semibold gap-x-1 hover:text-gray-800 dark:text-theme-app-50 dark:hover:bg-theme-app-300 !important;
}
.via-drawer-count {
@ -29,3 +29,12 @@
.via-icons-sub-sidebar {
@apply !text-theme-app-950 bg-cyan-50 rounded-md h-6 w-6 p-[1px] drop-shadow-md !important;
}
.via-input-search-sidebar {
@apply w-full rounded-l-lg shadow-lg !border-l-2 !border-y-2 border-r-0 border-[#7b1fa2] focus:border-blue-500 !important;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5) !important;
}
.via-button-search-sidebar {
@apply bg-theme-app-500 hover:bg-theme-app-600 text-white flex justify-center items-center p-2 rounded-r-lg cursor-pointer shadow-neon dark:!border-l-0 !important;
}

View File

@ -26,6 +26,58 @@
@apply bg-theme-app-700 border-theme-app-700 dark:bg-theme-app-900 dark:border-theme-app-900;
transition: 0s;
}
@keyframes pulse-neon {
0%, 100% {
box-shadow:
0 0 4px var(--tw-shadow-color),
0 0 8px var(--tw-shadow-color);
}
25% {
box-shadow:
0 0 8px var(--tw-shadow-color),
0 0 10px var(--tw-shadow-color);
}
50% {
box-shadow:
0 0 6px var(--tw-shadow-color),
0 0 12px var(--tw-shadow-color);
}
75% {
box-shadow:
0 0 8px var(--tw-shadow-color),
0 0 14px var(--tw-shadow-color);
}
}
}
@layer components {
.shadow-neon {
@apply shadow-lg;
border: 2px solid var(--tw-border-color);
--tw-shadow-color: rgba(0, 0, 0, 0.25);
--tw-border-color: transparent;
box-shadow: 0 2px 15px var(--tw-shadow-color);
}
.dark .shadow-neon {
--tw-shadow-color: #9c27b0;
--tw-border-color: #7b1fa2;
box-shadow: 0 2px 15px var(--tw-shadow-color);
}
.dark .shadow-neon:hover {
animation: pulse-neon 2s infinite ease-in-out;
}
.dark .shadow-neon.shadow-neon-purple {
--tw-shadow-color: #9c27b0;
--tw-border-color: #7b1fa2;
}
.dark .shadow-neon.shadow-neon-sky {
--tw-shadow-color: #00c4e2;
--tw-border-color: #00a2ba;
}
}
/* Additional Styles */

View File

@ -110,7 +110,7 @@
}
.responsive-container {
@apply w-full h-auto min-h-[calc(100vh-60px)] mx-auto bg-[#ECEFF8] dark:bg-[#0f0f0f] items-center pb-10 p-2;
@apply w-full h-auto min-h-[calc(100vh-60px)] mx-auto bg-[#ECEFF8] dark:bg-[#0f0f0f] items-center pb-10 p-2 transition-colors duration-[350ms] ease-in-out text-[#0F0F0F] dark:text-[#F1F1F1] !important;
}
.via-title {

View File

@ -11,7 +11,7 @@
}
.via-login-logo {
@apply w-64 h-64 bg-center bg-auto bg-no-repeat object-cover;
@apply w-64 h-64 bg-center bg-auto bg-no-repeat object-contain !important;
}
.via-login-background {

View File

@ -1,14 +1,14 @@
const theme = 'blue'
const presets = {
appTitle: 'Orbis Template',
appTitle: 'Orbis Playground',
theme: `${theme}`,
svgIconUrl: 'https://www.via-asesores.com/svgicons/smartoperation/',
images: {
loginFondo: 'https://www.via-asesores.com/backgrounds/smartoperation/SmartOperation_background.png',
loginFondo: `${process.env.publicPath}/images/backgrounds.webp`,
welcomeFondo: 'https://www.via-asesores.com/backgrounds/smartoperation/SmartOperation_background.png',
icon: 'https://www.via-asesores.com/logos/logo_icons/smarterp_icon.svg',
logo: 'https://www.via-asesores.com/logos/smartOperations.png',
icon: 'https://www.via-asesores.com/logos/logo_icons/smartdeveloper_icon.svg',
logo: 'https://www.via-asesores.com/logos/logo_vertical/smartdeveloper_vertical_logo.svg',
imageLoader: 'https://www.via-asesores.com/logos/logo_via.png',
noImageFound: 'https://www.via-asesores.com/smartsalesnprofit/images/LogoViasaClaroTransparente_600x.png',
onError: 'https://www.via-asesores.com/smartsalesnprofit/images/LogoViasaClaroTransparente_600x.png',
@ -25,8 +25,7 @@ const presets = {
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
theme: 'light'
draggable: true
},
pristine: {
// class of the parent element where the error/success class is added

View File

@ -54,6 +54,8 @@ const config: Config = {
md: '0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.02)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.01)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.01)'
// neon: '0 0 3px #7f11e0, 0 0 6px #7f11e0, 0 0 9px #7f11e0',
// 'neon-hover': '0 0 6px #7f11e0, 0 0 12px #7f11e0, 0 0 18px #7f11e0'
},
outline: {
blue: '2px solid rgba(0, 112, 244, 0.5)'
@ -64,17 +66,17 @@ const config: Config = {
disabled: '#cbd5e1'
},
'theme-app': {
50: '#DDE3EB',
100: '#B0C4D6',
200: '#83A5C1',
300: '#5686AC',
400: '#296897',
500: '#1E507B',
600: '#1B4870',
700: '#183F64',
800: '#153758',
900: '#112F4D',
950: '#0E2741'
50: '#e1ddeb',
100: '#bab0d6',
200: '#9383c1',
300: '#6c56ac',
400: '#462997',
500: '#361e7b',
600: '#311b70',
700: '#2c1864',
800: '#261558',
900: '#21114d',
950: '#1b0e41'
},
warning: '#3B82F6',
'warning-dark': '#1E40AF',