miolo 3.0.0-beta.21 → 3.0.0-beta.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/build/build.mjs +53 -0
- package/bin/build/build_bin.mjs +17 -0
- package/bin/build/cli/client.mjs +52 -0
- package/bin/build/cli/css.mjs +22 -0
- package/bin/build/cli/index.mjs +40 -0
- package/bin/build/cli/ssr.mjs +21 -0
- package/bin/build/server/aliases.mjs +112 -0
- package/bin/build/server/babel.config.js +24 -0
- package/bin/build/server/banner.mjs +20 -0
- package/bin/build/server/bundle.mjs +111 -0
- package/bin/build/server/fix.mjs +15 -0
- package/bin/build/server/index.mjs +69 -0
- package/bin/build/server/options.mjs +83 -0
- package/bin/create/auth.mjs +23 -0
- package/bin/create/copy.mjs +175 -0
- package/bin/create/docker.mjs +25 -0
- package/bin/create/index.mjs +137 -0
- package/bin/create/pkgjson.mjs +72 -0
- package/bin/create/prepare-template.mjs +158 -0
- package/bin/create/validation.mjs +27 -0
- package/bin/dev/dev.mjs +32 -23
- package/bin/dev/dev_start.mjs +6 -5
- package/bin/index.mjs +94 -52
- package/bin/prod-bin/create-bin.mjs +42 -27
- package/bin/prod-bin/run.mjs +13 -9
- package/bin/{prod-run → run}/pid.mjs +4 -4
- package/bin/run/restart.mjs +13 -0
- package/bin/run/start.mjs +18 -0
- package/bin/run/stop.mjs +20 -0
- package/bin/util.mjs +35 -11
- package/package.json +59 -39
- package/src/config/.env +34 -12
- package/src/config/defaults.mjs +253 -185
- package/src/config/env.mjs +40 -22
- package/src/config/index.mjs +19 -24
- package/src/config/util.mjs +25 -10
- package/src/db-conn.mjs +34 -0
- package/src/engines/cron/emails.mjs +10 -5
- package/src/engines/cron/index.mjs +45 -51
- package/src/engines/cron/init.mjs +16 -17
- package/src/engines/cron/ipsum.mjs +65 -60
- package/src/engines/cron/syscheck.mjs +30 -30
- package/src/engines/emailer/index.mjs +1 -2
- package/src/engines/emailer/queue.mjs +14 -20
- package/src/engines/emailer/transporter.mjs +86 -74
- package/src/engines/geoip/index.mjs +23 -28
- package/src/engines/http/index.mjs +26 -15
- package/src/engines/logger/buildErrorEmailBody.mjs +72 -0
- package/src/engines/logger/index.mjs +114 -122
- package/src/engines/logger/injectStackTrace.mjs +59 -0
- package/src/engines/logger/logger_mail.mjs +47 -61
- package/src/engines/logger/reopenTransportOnHupSignal.mjs +12 -13
- package/src/engines/parser/Parser.mjs +77 -60
- package/src/engines/parser/index.mjs +1 -1
- package/src/engines/schema/diffObjs.mjs +41 -0
- package/src/engines/schema/index.mjs +4 -0
- package/src/engines/schema/input.mjs +54 -0
- package/src/engines/schema/output.mjs +66 -0
- package/src/engines/socket/index.mjs +44 -46
- package/src/index.mjs +15 -10
- package/src/middleware/auth/basic.mjs +41 -40
- package/src/middleware/auth/custom.mjs +10 -13
- package/src/middleware/auth/guest.mjs +27 -27
- package/src/middleware/auth/passport/index.mjs +374 -0
- package/src/middleware/auth/passport/session/index.mjs +43 -0
- package/src/middleware/auth/{credentials → passport}/session/store.mjs +35 -15
- package/src/middleware/auth/passport/session/store_koa_redis.mjs +3 -0
- package/src/middleware/context/cache/index.mjs +78 -33
- package/src/middleware/context/cache/options.mjs +19 -21
- package/src/middleware/context/db.mjs +45 -20
- package/src/middleware/context/index.mjs +12 -12
- package/src/middleware/extra.mjs +4 -5
- package/src/middleware/http/body.mjs +25 -25
- package/src/middleware/http/catcher.mjs +81 -8
- package/src/middleware/http/custom_blacklist.mjs +19 -16
- package/src/middleware/http/headers.mjs +37 -34
- package/src/middleware/http/ratelimit.mjs +16 -23
- package/src/middleware/http/request.mjs +60 -65
- package/src/middleware/routes/catch_js_error.mjs +30 -23
- package/src/middleware/routes/robots.mjs +4 -7
- package/src/middleware/routes/router/crud/attachCrudRoutes.mjs +108 -90
- package/src/middleware/routes/router/crud/getCrudConfig.mjs +31 -55
- package/src/middleware/routes/router/defaults.mjs +6 -19
- package/src/middleware/routes/router/index.mjs +17 -21
- package/src/middleware/routes/router/queries/attachQueriesRoutes.mjs +227 -50
- package/src/middleware/routes/router/queries/getQueriesConfig.mjs +45 -55
- package/src/middleware/routes/router/utils.mjs +41 -26
- package/src/middleware/ssr/context.mjs +5 -7
- package/src/middleware/ssr/html.mjs +66 -43
- package/src/middleware/ssr/loader.mjs +11 -14
- package/src/middleware/ssr/ssr_render.mjs +39 -22
- package/src/middleware/static/index.mjs +33 -14
- package/src/middleware/vite/devserver.mjs +38 -22
- package/src/middleware/vite/watcher.mjs +12 -14
- package/src/server-cron.mjs +13 -8
- package/src/server-dev.mjs +13 -16
- package/src/server.mjs +49 -51
- package/template/.agent/skills/miolo-app-arch/SKILL.md +218 -0
- package/template/.agent/skills/miolo-auth/SKILL.md +450 -0
- package/template/.agent/skills/miolo-cli-router/SKILL.md +394 -0
- package/template/.agent/skills/miolo-database/SKILL.md +358 -0
- package/template/.agent/skills/miolo-react-patterns/SKILL.md +426 -0
- package/template/.agent/skills/miolo-routing/SKILL.md +326 -0
- package/template/.agent/skills/miolo-schemas/SKILL.md +329 -0
- package/template/.agent/skills/miolo-session-context/SKILL.md +397 -0
- package/template/.agent/skills/miolo-ssr/SKILL.md +433 -0
- package/template/.editorconfig +18 -0
- package/template/.env +120 -0
- package/template/biome.json +63 -0
- package/template/components.json +21 -0
- package/template/db/init.sh +89 -0
- package/template/db/sql/00_drop.sql +2 -0
- package/template/db/sql/01_users.sql +31 -0
- package/template/db/sql/02_todos.sql +20 -0
- package/template/docker/Dockerfile +13 -0
- package/template/docker/docker-compose.yaml +79 -0
- package/template/gitignore +42 -0
- package/template/jsconfig.json +18 -0
- package/template/package.json +88 -0
- package/template/postcss.config.js +9 -0
- package/template/src/cli/App.jsx +25 -0
- package/template/src/cli/components/JsonTreeViewer.jsx +128 -0
- package/template/src/cli/components/shadcn-io/spinner/index.jsx +232 -0
- package/template/src/cli/components/stepper.jsx +408 -0
- package/template/src/cli/components/ui/avatar.jsx +36 -0
- package/template/src/cli/components/ui/badge.jsx +31 -0
- package/template/src/cli/components/ui/breadcrumb.jsx +97 -0
- package/template/src/cli/components/ui/card.jsx +73 -0
- package/template/src/cli/components/ui/collapsible.jsx +16 -0
- package/template/src/cli/components/ui/dropdown-menu.jsx +179 -0
- package/template/src/cli/components/ui/field.jsx +217 -0
- package/template/src/cli/components/ui/input.jsx +19 -0
- package/template/src/cli/components/ui/label.jsx +17 -0
- package/template/src/cli/components/ui/pagination.jsx +99 -0
- package/template/src/cli/components/ui/patched/alert.jsx +56 -0
- package/template/src/cli/components/ui/patched/button.jsx +45 -0
- package/template/src/cli/components/ui/patched/dialog.jsx +114 -0
- package/template/src/cli/components/ui/patched/sidebar.jsx +660 -0
- package/template/src/cli/components/ui/select.jsx +141 -0
- package/template/src/cli/components/ui/separator.jsx +21 -0
- package/template/src/cli/components/ui/sheet.jsx +115 -0
- package/template/src/cli/components/ui/skeleton.jsx +13 -0
- package/template/src/cli/components/ui/sonner.jsx +22 -0
- package/template/src/cli/components/ui/switch.jsx +25 -0
- package/template/src/cli/components/ui/table.jsx +88 -0
- package/template/src/cli/components/ui/textarea.jsx +16 -0
- package/template/src/cli/components/ui/tooltip.jsx +45 -0
- package/template/src/cli/config/store_keys.mjs +2 -0
- package/template/src/cli/context/data/DataContext.jsx +5 -0
- package/template/src/cli/context/data/DataProvider.jsx +44 -0
- package/template/src/cli/context/data/useBreads.mjs +15 -0
- package/template/src/cli/context/data/useDataContext.mjs +4 -0
- package/template/src/cli/context/session/SessionContext.mjs +4 -0
- package/template/src/cli/context/session/SessionProvider.jsx +31 -0
- package/template/src/cli/context/session/makePermissioner.mjs +34 -0
- package/template/src/cli/context/session/useSessionContext.mjs +6 -0
- package/template/src/cli/context/theme/ThemeContext.mjs +4 -0
- package/template/src/cli/context/theme/ThemeProvider.jsx +49 -0
- package/template/src/cli/context/theme/useThemeContext.mjs +6 -0
- package/template/src/cli/context/ui/UIContext.jsx +5 -0
- package/template/src/cli/context/ui/UIProvider.jsx +16 -0
- package/template/src/cli/context/ui/useUIContext.mjs +4 -0
- package/template/src/cli/context/util.mjs +17 -0
- package/template/src/cli/entry-cli.jsx +33 -0
- package/template/src/cli/hooks/useIsMobile.mjs +19 -0
- package/template/src/cli/hooks/useStoragedState.mjs +63 -0
- package/template/src/cli/index.html +29 -0
- package/template/src/cli/layout/app-sidebar.jsx +25 -0
- package/template/src/cli/layout/main-layout.jsx +63 -0
- package/template/src/cli/layout/nav-last-todos.jsx +72 -0
- package/template/src/cli/layout/nav-main.jsx +39 -0
- package/template/src/cli/layout/nav-user.jsx +105 -0
- package/template/src/cli/layout/prop-switcher.jsx +93 -0
- package/template/src/cli/lib/utils.mjs +10 -0
- package/template/src/cli/pages/Index.jsx +13 -0
- package/template/src/cli/pages/IndexOffline.jsx +13 -0
- package/template/src/cli/pages/IndexOnline.jsx +18 -0
- package/template/src/cli/pages/dash/Dashboard.jsx +29 -0
- package/template/src/cli/pages/offline/Login.jsx +43 -0
- package/template/src/cli/pages/offline/LoginForm.jsx +115 -0
- package/template/src/cli/pages/security/Security.jsx +39 -0
- package/template/src/cli/pages/security/SecurityForm.jsx +106 -0
- package/template/src/cli/pages/todos/TodoActions.jsx +99 -0
- package/template/src/cli/pages/todos/TodoAdd.jsx +43 -0
- package/template/src/cli/pages/todos/TodoList.jsx +60 -0
- package/template/src/cli/pages/todos/Todos.jsx +23 -0
- package/template/src/cli/pages/todos/context/TodosContext.jsx +5 -0
- package/template/src/cli/pages/todos/context/TodosProvider.jsx +191 -0
- package/template/src/cli/pages/todos/context/useTodosContext.mjs +4 -0
- package/template/src/ns/models/Todo.mjs +29 -0
- package/template/src/ns/models/TodoList.mjs +8 -0
- package/template/src/ns/models/User.mjs +40 -0
- package/template/src/server/bot/check_today.mjs +10 -0
- package/template/src/server/io/cache/base.mjs +21 -0
- package/template/src/server/io/db/filter.mjs +92 -0
- package/template/src/server/io/db/todos/delete.mjs +29 -0
- package/template/src/server/io/db/todos/find.mjs +13 -0
- package/template/src/server/io/db/todos/read.mjs +83 -0
- package/template/src/server/io/db/todos/toggle.mjs +37 -0
- package/template/src/server/io/db/todos/upsave.mjs +32 -0
- package/template/src/server/io/db/triggers/user.mjs +13 -0
- package/template/src/server/io/db/users/auth.mjs +132 -0
- package/template/src/server/io/db/users/pwd.mjs +38 -0
- package/template/src/server/io/db/users/save.mjs +17 -0
- package/template/src/server/miolo/auth/basic.mjs +15 -0
- package/template/src/server/miolo/auth/guest.mjs +3 -0
- package/template/src/server/miolo/auth/passport.mjs +73 -0
- package/template/src/server/miolo/cache.mjs +11 -0
- package/template/src/server/miolo/cron/foo.mjs +7 -0
- package/template/src/server/miolo/cron/index.mjs +28 -0
- package/template/src/server/miolo/cron/invalidate.mjs +21 -0
- package/template/src/server/miolo/db.mjs +36 -0
- package/template/src/server/miolo/http.mjs +14 -0
- package/template/src/server/miolo/index.mjs +43 -0
- package/template/src/server/miolo/routes/crud.mjs +16 -0
- package/template/src/server/miolo/routes/index.mjs +8 -0
- package/template/src/server/miolo/ssr/entry-server.jsx +13 -0
- package/template/src/server/miolo/ssr/loader.mjs +18 -0
- package/template/src/server/routes/index.mjs +66 -0
- package/template/src/server/routes/todos/mod.mjs +52 -0
- package/template/src/server/routes/todos/read.mjs +45 -0
- package/template/src/server/routes/todos/special.mjs +47 -0
- package/template/src/server/routes/users/user.mjs +54 -0
- package/template/src/server/server.mjs +10 -0
- package/template/src/server/utils/crypt.mjs +38 -0
- package/template/src/server/utils/io.mjs +15 -0
- package/template/src/server/utils/pwdfor.mjs +25 -0
- package/template/src/server/utils/schema.mjs +22 -0
- package/template/src/static/img/default/profile.png +0 -0
- package/template/src/static/img/favicon.ico +0 -0
- package/template/src/static/img/miolo_logo.png +0 -0
- package/template/src/static/img/miolo_name.png +0 -0
- package/template/src/static/public/manifest.json +21 -0
- package/template/src/static/public/sw.js +79 -0
- package/template/src/static/style/globals.css +156 -0
- package/template/src/static/style/json-tree.css +54 -0
- package/template/src/static/style/skeleton.css +49 -0
- package/bin/prod-build/build-client.mjs +0 -67
- package/bin/prod-build/build-server.mjs +0 -58
- package/bin/prod-run/restart.mjs +0 -9
- package/bin/prod-run/start.mjs +0 -15
- package/bin/prod-run/stop.mjs +0 -20
- package/src/engines/logger/verify.mjs +0 -22
- package/src/middleware/auth/credentials/index.mjs +0 -151
- package/src/middleware/auth/credentials/session/index.mjs +0 -24
- package/src/middleware/auth/credentials/session/store_koa_redis.mjs +0 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { THEME_STORAGE_KEY } from "#cli/config/store_keys.mjs"
|
|
3
|
+
import ThemeContext from "./ThemeContext.mjs"
|
|
4
|
+
|
|
5
|
+
export default function ThemeProvider({ children, defaultTheme = "system" }) {
|
|
6
|
+
const [theme, setTheme] = useState(() =>
|
|
7
|
+
typeof localStorage !== "undefined"
|
|
8
|
+
? localStorage.getItem(THEME_STORAGE_KEY) || defaultTheme
|
|
9
|
+
: defaultTheme
|
|
10
|
+
)
|
|
11
|
+
const [isDark, setIsDark] = useState(false)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const root = window.document.documentElement
|
|
15
|
+
|
|
16
|
+
root.classList.remove("light", "dark")
|
|
17
|
+
|
|
18
|
+
if (theme === "system") {
|
|
19
|
+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
20
|
+
? "dark"
|
|
21
|
+
: "light"
|
|
22
|
+
|
|
23
|
+
root.classList.add(systemTheme)
|
|
24
|
+
setIsDark(systemTheme === "dark")
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
root.classList.add(theme)
|
|
29
|
+
setIsDark(theme === "dark")
|
|
30
|
+
}, [theme])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<ThemeContext.Provider
|
|
34
|
+
value={{
|
|
35
|
+
theme,
|
|
36
|
+
setTheme: (theme) => {
|
|
37
|
+
if (typeof localStorage !== "undefined") {
|
|
38
|
+
localStorage.setItem(THEME_STORAGE_KEY, theme)
|
|
39
|
+
}
|
|
40
|
+
setTheme(theme)
|
|
41
|
+
},
|
|
42
|
+
isDark,
|
|
43
|
+
isLight: () => !isDark
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</ThemeContext.Provider>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { toast } from "sonner"
|
|
2
|
+
import UIContext from "./UIContext.jsx"
|
|
3
|
+
|
|
4
|
+
const UIProvider = ({ children }) => {
|
|
5
|
+
return (
|
|
6
|
+
<UIContext.Provider
|
|
7
|
+
value={{
|
|
8
|
+
toast
|
|
9
|
+
}}
|
|
10
|
+
>
|
|
11
|
+
{children}
|
|
12
|
+
</UIContext.Provider>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default UIProvider
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function isBrowser() {
|
|
2
|
+
return typeof window !== "undefined"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isDEVAndVite() {
|
|
6
|
+
if (isBrowser()) {
|
|
7
|
+
return window.__CONTEXT?.development === true
|
|
8
|
+
}
|
|
9
|
+
return process.env.NODE_ENV === "development"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function imgSrc(url) {
|
|
13
|
+
// if (isDEVAndVite()) {
|
|
14
|
+
// return `http://localhost:8005/${url.startsWith("/") ? url.slice(1) : url}`
|
|
15
|
+
// }
|
|
16
|
+
return url
|
|
17
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AppBrowser } from "miolo-react"
|
|
2
|
+
import { hydrateRoot } from "react-dom/client"
|
|
3
|
+
import { BrowserRouter } from "react-router"
|
|
4
|
+
import App from "./App.jsx"
|
|
5
|
+
|
|
6
|
+
import "../static/style/globals.css"
|
|
7
|
+
|
|
8
|
+
const domNode = document.getElementById("root")
|
|
9
|
+
|
|
10
|
+
hydrateRoot(
|
|
11
|
+
domNode,
|
|
12
|
+
<AppBrowser>
|
|
13
|
+
<BrowserRouter>
|
|
14
|
+
<App />
|
|
15
|
+
</BrowserRouter>
|
|
16
|
+
</AppBrowser>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// Comprobamos si el navegador del usuario soporta Service Workers
|
|
20
|
+
if ("serviceWorker" in navigator) {
|
|
21
|
+
// Esperamos a que la página cargue completamente para no afectar el rendimiento inicial
|
|
22
|
+
window.addEventListener("load", () => {
|
|
23
|
+
// Apuntamos a la URL pública donde Miolo está sirviendo el archivo
|
|
24
|
+
navigator.serviceWorker
|
|
25
|
+
.register("/sw.js")
|
|
26
|
+
.then((registration) => {
|
|
27
|
+
console.log("Service Worker registrado con éxito. Scope:", registration.scope)
|
|
28
|
+
})
|
|
29
|
+
.catch((error) => {
|
|
30
|
+
console.error("Fallo al registrar el Service Worker:", error)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export default function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = useState(undefined)
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener("change", onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener("change", onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect, useState /*useCallback,*/ } from "react"
|
|
2
|
+
import { isBrowser } from "#cli/lib/utils.mjs"
|
|
3
|
+
|
|
4
|
+
const getStorageKey = (key) => {
|
|
5
|
+
if (isBrowser()) {
|
|
6
|
+
return `${encodeURIComponent(window.location.pathname)}_${key || ""}`
|
|
7
|
+
}
|
|
8
|
+
return undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const getPersisted = (key, defValue) => {
|
|
12
|
+
if (isBrowser()) {
|
|
13
|
+
const k = getStorageKey(key)
|
|
14
|
+
const v = localStorage.getItem(k)
|
|
15
|
+
try {
|
|
16
|
+
const p = JSON.parse(v)
|
|
17
|
+
if (p !== undefined && p !== null) {
|
|
18
|
+
return p
|
|
19
|
+
}
|
|
20
|
+
} catch (_) {}
|
|
21
|
+
}
|
|
22
|
+
return defValue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const setPersisted = (key, value) => {
|
|
26
|
+
if (isBrowser()) {
|
|
27
|
+
const k = getStorageKey(key)
|
|
28
|
+
|
|
29
|
+
localStorage.setItem(k, JSON.stringify(value))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const useStoragedState = (defValue, key) => {
|
|
34
|
+
const [value, setValue] = useState(getPersisted(key, defValue))
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
const setStoragedValue = useCallback((newValue) => {
|
|
38
|
+
setPersisted(key, newValue)
|
|
39
|
+
setValue(newValue)
|
|
40
|
+
}, [key])
|
|
41
|
+
return [value, setStoragedValue]
|
|
42
|
+
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
//
|
|
46
|
+
// Returning and exposing setValue, we allow
|
|
47
|
+
// to use functional setValue() too!
|
|
48
|
+
//
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setPersisted(key, value)
|
|
52
|
+
}, [key, value])
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
const wrapSetValue = useCallback((nValue) => {
|
|
56
|
+
setPersisted(key, typeof nValue =='function' ? nValue(value) : nValue)
|
|
57
|
+
setValue(nValue)
|
|
58
|
+
}, [key, value])*/
|
|
59
|
+
|
|
60
|
+
return [value, setValue]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { useStoragedState }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
7
|
+
<title>miolo-sample</title>
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
9
|
+
<meta name="description" content="miolo-sample" />
|
|
10
|
+
<meta name="keywords" content="miolo-sample" />
|
|
11
|
+
<meta name="author" content="devel@afialapis.com" />
|
|
12
|
+
|
|
13
|
+
<!-- Touch Icons - iOS and Android 2.1+ 180x180 pixels in size. -->
|
|
14
|
+
<!--<link rel="apple-touch-icon-precomposed" href="/favicon.ico"/>-->
|
|
15
|
+
<!-- Firefox, Chrome, Safari, IE 11+ and Opera. 196x196 pixels in size. -->
|
|
16
|
+
<link rel="icon" href="/favicon.ico" />
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
<!-- PWA -->
|
|
21
|
+
<link rel="manifest" href="/manifest.json">
|
|
22
|
+
<meta name="theme-color" content="#fb64b6">
|
|
23
|
+
|
|
24
|
+
</head>
|
|
25
|
+
|
|
26
|
+
<body>
|
|
27
|
+
</body>
|
|
28
|
+
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import {
|
|
3
|
+
Sidebar,
|
|
4
|
+
SidebarContent,
|
|
5
|
+
SidebarFooter,
|
|
6
|
+
SidebarRail
|
|
7
|
+
} from "#cli/components/ui/patched/sidebar.jsx"
|
|
8
|
+
import { NavLastTodos } from "./nav-last-todos.jsx"
|
|
9
|
+
import { NavMain } from "./nav-main.jsx"
|
|
10
|
+
import { NavUser } from "./nav-user.jsx"
|
|
11
|
+
|
|
12
|
+
export function AppSidebar() {
|
|
13
|
+
return (
|
|
14
|
+
<Sidebar collapsible="icon">
|
|
15
|
+
<SidebarContent>
|
|
16
|
+
<NavLastTodos />
|
|
17
|
+
<NavMain />
|
|
18
|
+
</SidebarContent>
|
|
19
|
+
<SidebarFooter>
|
|
20
|
+
<NavUser />
|
|
21
|
+
</SidebarFooter>
|
|
22
|
+
<SidebarRail />
|
|
23
|
+
</Sidebar>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Fragment } from "react"
|
|
2
|
+
import { Outlet } from "react-router"
|
|
3
|
+
import { Spinner } from "#cli/components/shadcn-io/spinner/index.jsx"
|
|
4
|
+
import {
|
|
5
|
+
Breadcrumb,
|
|
6
|
+
BreadcrumbItem,
|
|
7
|
+
BreadcrumbLink,
|
|
8
|
+
BreadcrumbList,
|
|
9
|
+
BreadcrumbPage,
|
|
10
|
+
BreadcrumbSeparator
|
|
11
|
+
} from "#cli/components/ui/breadcrumb.jsx"
|
|
12
|
+
import {
|
|
13
|
+
SidebarInset,
|
|
14
|
+
SidebarProvider,
|
|
15
|
+
SidebarTrigger
|
|
16
|
+
} from "#cli/components/ui/patched/sidebar.jsx"
|
|
17
|
+
import { Separator } from "#cli/components/ui/separator.jsx"
|
|
18
|
+
import { Toaster } from "#cli/components/ui/sonner.jsx"
|
|
19
|
+
import useDataContext from "#cli/context/data/useDataContext.mjs"
|
|
20
|
+
import { AppSidebar } from "#cli/layout/app-sidebar.jsx"
|
|
21
|
+
|
|
22
|
+
export default function MainLayout() {
|
|
23
|
+
const { breads, loading } = useDataContext()
|
|
24
|
+
|
|
25
|
+
const hasBreads = breads.length > 0
|
|
26
|
+
const firstBreads = breads.length > 1 ? breads.slice(0, -1) : []
|
|
27
|
+
const lastBread = hasBreads ? breads[breads.length - 1] : []
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<SidebarProvider>
|
|
31
|
+
<AppSidebar />
|
|
32
|
+
<SidebarInset>
|
|
33
|
+
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
|
34
|
+
<SidebarTrigger className="-ml-1" />
|
|
35
|
+
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
|
36
|
+
{hasBreads && (
|
|
37
|
+
<Breadcrumb>
|
|
38
|
+
<BreadcrumbList>
|
|
39
|
+
{firstBreads.map((bread) => {
|
|
40
|
+
return (
|
|
41
|
+
<Fragment key={`bread_${bread[1]}`}>
|
|
42
|
+
<BreadcrumbItem className="hidden md:block">
|
|
43
|
+
<BreadcrumbLink href={bread[0]}>{bread[1]}</BreadcrumbLink>
|
|
44
|
+
</BreadcrumbItem>
|
|
45
|
+
<BreadcrumbSeparator className="hidden md:block" />
|
|
46
|
+
</Fragment>
|
|
47
|
+
)
|
|
48
|
+
})}
|
|
49
|
+
<BreadcrumbItem>
|
|
50
|
+
<BreadcrumbPage>
|
|
51
|
+
{lastBread.length > 1 ? lastBread[1] : lastBread[0]}
|
|
52
|
+
</BreadcrumbPage>
|
|
53
|
+
</BreadcrumbItem>
|
|
54
|
+
</BreadcrumbList>
|
|
55
|
+
</Breadcrumb>
|
|
56
|
+
)}
|
|
57
|
+
</header>
|
|
58
|
+
{loading ? <Spinner /> : <Outlet />}
|
|
59
|
+
<Toaster />
|
|
60
|
+
</SidebarInset>
|
|
61
|
+
</SidebarProvider>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { Cable, ChartSpline, ChevronRight, HousePlug, Route, RouteOff } from "lucide-react"
|
|
3
|
+
import { Link } from "react-router"
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Collapsible,
|
|
7
|
+
CollapsibleContent,
|
|
8
|
+
CollapsibleTrigger
|
|
9
|
+
} from "#cli/components/ui/collapsible.jsx"
|
|
10
|
+
import {
|
|
11
|
+
SidebarGroup,
|
|
12
|
+
SidebarGroupLabel,
|
|
13
|
+
SidebarMenu,
|
|
14
|
+
SidebarMenuButton,
|
|
15
|
+
SidebarMenuItem,
|
|
16
|
+
SidebarMenuSub,
|
|
17
|
+
SidebarMenuSubButton,
|
|
18
|
+
SidebarMenuSubItem
|
|
19
|
+
} from "#cli/components/ui/patched/sidebar.jsx"
|
|
20
|
+
import useDataContext from "#cli/context/data/useDataContext.mjs"
|
|
21
|
+
|
|
22
|
+
export function NavLastTodos() {
|
|
23
|
+
const { lastTodos } = useDataContext()
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<SidebarGroup>
|
|
27
|
+
<SidebarGroupLabel>Resumen</SidebarGroupLabel>
|
|
28
|
+
<SidebarMenu>
|
|
29
|
+
<SidebarMenuItem>
|
|
30
|
+
<SidebarMenuButton tooltip={"Inicio"}>
|
|
31
|
+
<ChartSpline />
|
|
32
|
+
<Link to="/">Inicio</Link>
|
|
33
|
+
</SidebarMenuButton>
|
|
34
|
+
</SidebarMenuItem>
|
|
35
|
+
{lastTodos.length === 0 ? null : (
|
|
36
|
+
<Collapsible key={"Conectadas"} asChild defaultOpen={true} className="group/collapsible">
|
|
37
|
+
<SidebarMenuItem>
|
|
38
|
+
<CollapsibleTrigger asChild>
|
|
39
|
+
<SidebarMenuButton tooltip={"Últimos todos"}>
|
|
40
|
+
<HousePlug />
|
|
41
|
+
<span>Últimos todos</span>
|
|
42
|
+
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
43
|
+
</SidebarMenuButton>
|
|
44
|
+
</CollapsibleTrigger>
|
|
45
|
+
<CollapsibleContent>
|
|
46
|
+
<SidebarMenuSub>
|
|
47
|
+
{lastTodos.map((todo) => (
|
|
48
|
+
<SidebarMenuSubItem key={todo.id}>
|
|
49
|
+
<SidebarMenuSubButton asChild>
|
|
50
|
+
<Link to={`/`}>
|
|
51
|
+
{todo.done ? <RouteOff color="red" /> : <Route color="green" />}
|
|
52
|
+
<span>{todo.description}</span>
|
|
53
|
+
</Link>
|
|
54
|
+
</SidebarMenuSubButton>
|
|
55
|
+
</SidebarMenuSubItem>
|
|
56
|
+
))}
|
|
57
|
+
</SidebarMenuSub>
|
|
58
|
+
</CollapsibleContent>
|
|
59
|
+
</SidebarMenuItem>
|
|
60
|
+
</Collapsible>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<SidebarMenuItem>
|
|
64
|
+
<SidebarMenuButton tooltip={"Yay!"}>
|
|
65
|
+
<Cable />
|
|
66
|
+
<Link to="/">Yay!</Link>
|
|
67
|
+
</SidebarMenuButton>
|
|
68
|
+
</SidebarMenuItem>
|
|
69
|
+
</SidebarMenu>
|
|
70
|
+
</SidebarGroup>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { ArrowDownUp, Braces } from "lucide-react"
|
|
3
|
+
import { Link } from "react-router"
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
SidebarGroup,
|
|
7
|
+
SidebarGroupLabel,
|
|
8
|
+
SidebarMenu,
|
|
9
|
+
SidebarMenuButton,
|
|
10
|
+
SidebarMenuItem
|
|
11
|
+
} from "#cli/components/ui/patched/sidebar.jsx"
|
|
12
|
+
// import useSessionContext from '#cli/context/session/useSessionContext.mjs'
|
|
13
|
+
|
|
14
|
+
export function NavMain() {
|
|
15
|
+
// const {permiss} = useSessionContext()
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<SidebarGroup>
|
|
19
|
+
<SidebarGroupLabel>Configuración</SidebarGroupLabel>
|
|
20
|
+
<SidebarMenu>
|
|
21
|
+
<SidebarMenuItem>
|
|
22
|
+
<SidebarMenuButton tooltip={"Red"}>
|
|
23
|
+
<ArrowDownUp />
|
|
24
|
+
<Link to="/">Foo</Link>
|
|
25
|
+
</SidebarMenuButton>
|
|
26
|
+
</SidebarMenuItem>
|
|
27
|
+
|
|
28
|
+
{/*permiss.can_user_debug() && (*/}
|
|
29
|
+
<SidebarMenuItem>
|
|
30
|
+
<SidebarMenuButton tooltip={"Dev"}>
|
|
31
|
+
<Braces />
|
|
32
|
+
<Link to="/dev">Bar</Link>
|
|
33
|
+
</SidebarMenuButton>
|
|
34
|
+
</SidebarMenuItem>
|
|
35
|
+
{/*)*/}
|
|
36
|
+
</SidebarMenu>
|
|
37
|
+
</SidebarGroup>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ChevronsUpDown, LogOut, Moon, Shield, Sun } from "lucide-react"
|
|
4
|
+
import { useNavigate } from "react-router"
|
|
5
|
+
import { Avatar, AvatarFallback, AvatarImage } from "#cli/components/ui/avatar.jsx"
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuLabel,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger
|
|
13
|
+
} from "#cli/components/ui/dropdown-menu.jsx"
|
|
14
|
+
import {
|
|
15
|
+
SidebarMenu,
|
|
16
|
+
SidebarMenuButton,
|
|
17
|
+
SidebarMenuItem,
|
|
18
|
+
useSidebar
|
|
19
|
+
} from "#cli/components/ui/patched/sidebar.jsx"
|
|
20
|
+
import useSessionContext from "#cli/context/session/useSessionContext.mjs"
|
|
21
|
+
import useThemeContext from "#cli/context/theme/useThemeContext.mjs"
|
|
22
|
+
|
|
23
|
+
export function NavUser() {
|
|
24
|
+
const { isMobile } = useSidebar()
|
|
25
|
+
|
|
26
|
+
const { session, logout, authMethod } = useSessionContext()
|
|
27
|
+
const navigate = useNavigate()
|
|
28
|
+
const { isDark, setTheme } = useThemeContext()
|
|
29
|
+
|
|
30
|
+
const twoLetters = session.name.slice(0, 2).toUpperCase()
|
|
31
|
+
const avatarSrc = `/static/img/default/profile.png`
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<SidebarMenu>
|
|
35
|
+
<SidebarMenuItem>
|
|
36
|
+
<DropdownMenu>
|
|
37
|
+
<DropdownMenuTrigger asChild>
|
|
38
|
+
<SidebarMenuButton
|
|
39
|
+
size="lg"
|
|
40
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
41
|
+
>
|
|
42
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
43
|
+
<AvatarImage src={avatarSrc} alt={session.name} />
|
|
44
|
+
<AvatarFallback className="rounded-lg">{twoLetters}</AvatarFallback>
|
|
45
|
+
</Avatar>
|
|
46
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
47
|
+
<span className="truncate font-medium">{session.name}</span>
|
|
48
|
+
<span className="truncate text-xs">{session.email}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<ChevronsUpDown className="ml-auto size-4" />
|
|
51
|
+
</SidebarMenuButton>
|
|
52
|
+
</DropdownMenuTrigger>
|
|
53
|
+
<DropdownMenuContent
|
|
54
|
+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
55
|
+
side={isMobile ? "bottom" : "right"}
|
|
56
|
+
align="end"
|
|
57
|
+
sideOffset={4}
|
|
58
|
+
>
|
|
59
|
+
<DropdownMenuLabel className="p-0 font-normal">
|
|
60
|
+
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
61
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
62
|
+
<AvatarImage src={avatarSrc} alt={session.name} />
|
|
63
|
+
<AvatarFallback className="rounded-lg">{twoLetters}</AvatarFallback>
|
|
64
|
+
</Avatar>
|
|
65
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
66
|
+
<span className="truncate font-medium">{session.name}</span>
|
|
67
|
+
<span className="truncate text-xs">{session.email}</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</DropdownMenuLabel>
|
|
71
|
+
<DropdownMenuSeparator />
|
|
72
|
+
{isDark ? (
|
|
73
|
+
<DropdownMenuItem onSelect={() => setTheme("light")}>
|
|
74
|
+
<Sun />
|
|
75
|
+
Modo día
|
|
76
|
+
</DropdownMenuItem>
|
|
77
|
+
) : (
|
|
78
|
+
<DropdownMenuItem onSelect={() => setTheme("dark")}>
|
|
79
|
+
<Moon />
|
|
80
|
+
Modo noche
|
|
81
|
+
</DropdownMenuItem>
|
|
82
|
+
)}
|
|
83
|
+
<DropdownMenuSeparator />
|
|
84
|
+
{/*
|
|
85
|
+
<DropdownMenuItem onSelect={() => navigate('/profile')}>
|
|
86
|
+
<User/>
|
|
87
|
+
Perfil
|
|
88
|
+
</DropdownMenuItem>
|
|
89
|
+
*/}
|
|
90
|
+
<DropdownMenuItem onSelect={() => navigate("/security")}>
|
|
91
|
+
<Shield />
|
|
92
|
+
Seguridad
|
|
93
|
+
</DropdownMenuItem>
|
|
94
|
+
{["local", "google", "basic"].includes(authMethod) && (
|
|
95
|
+
<DropdownMenuItem onSelect={() => logout()}>
|
|
96
|
+
<LogOut />
|
|
97
|
+
Cerrar sesión
|
|
98
|
+
</DropdownMenuItem>
|
|
99
|
+
)}
|
|
100
|
+
</DropdownMenuContent>
|
|
101
|
+
</DropdownMenu>
|
|
102
|
+
</SidebarMenuItem>
|
|
103
|
+
</SidebarMenu>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Cable, ChevronsUpDown, House } from "lucide-react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
import { useLocation } from "react-router"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuSeparator,
|
|
13
|
+
//DropdownMenuShortcut,
|
|
14
|
+
DropdownMenuTrigger
|
|
15
|
+
} from "#cli/components/ui/dropdown-menu.jsx"
|
|
16
|
+
import {
|
|
17
|
+
SidebarMenu,
|
|
18
|
+
SidebarMenuButton,
|
|
19
|
+
SidebarMenuItem,
|
|
20
|
+
useSidebar
|
|
21
|
+
} from "#cli/components/ui/patched/sidebar.jsx"
|
|
22
|
+
import useDataContext from "#cli/context/data/useDataContext.mjs"
|
|
23
|
+
|
|
24
|
+
export function PropSwitcher() {
|
|
25
|
+
const { isMobile } = useSidebar()
|
|
26
|
+
const location = useLocation()
|
|
27
|
+
|
|
28
|
+
const { properties } = useDataContext()
|
|
29
|
+
const [activeProp, setActiveProp] = useState(properties[0])
|
|
30
|
+
|
|
31
|
+
// TODO CHeck w
|
|
32
|
+
if (location.pathname === "/connect") {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!activeProp) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<SidebarMenu>
|
|
42
|
+
<SidebarMenuItem>
|
|
43
|
+
<DropdownMenu>
|
|
44
|
+
<DropdownMenuTrigger asChild>
|
|
45
|
+
<SidebarMenuButton
|
|
46
|
+
size="lg"
|
|
47
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
48
|
+
>
|
|
49
|
+
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
|
50
|
+
<House className="size-4" />
|
|
51
|
+
</div>
|
|
52
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
53
|
+
<span className="truncate font-medium">{activeProp.name}</span>
|
|
54
|
+
{/*<span className="truncate text-xs">{activeProp.plan}</span>*/}
|
|
55
|
+
</div>
|
|
56
|
+
<ChevronsUpDown className="ml-auto" />
|
|
57
|
+
</SidebarMenuButton>
|
|
58
|
+
</DropdownMenuTrigger>
|
|
59
|
+
<DropdownMenuContent
|
|
60
|
+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
61
|
+
align="start"
|
|
62
|
+
side={isMobile ? "bottom" : "right"}
|
|
63
|
+
sideOffset={4}
|
|
64
|
+
>
|
|
65
|
+
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
66
|
+
Propiedades
|
|
67
|
+
</DropdownMenuLabel>
|
|
68
|
+
{properties.map((prop, _index) => (
|
|
69
|
+
<DropdownMenuItem
|
|
70
|
+
key={prop.name}
|
|
71
|
+
onClick={() => setActiveProp(prop)}
|
|
72
|
+
className="gap-2 p-2"
|
|
73
|
+
>
|
|
74
|
+
<div className="flex size-6 items-center justify-center rounded-md border">
|
|
75
|
+
<House className="size-3.5 shrink-0" />
|
|
76
|
+
</div>
|
|
77
|
+
{prop.name}
|
|
78
|
+
{/*<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>*/}
|
|
79
|
+
</DropdownMenuItem>
|
|
80
|
+
))}
|
|
81
|
+
<DropdownMenuSeparator />
|
|
82
|
+
<DropdownMenuItem className="gap-2 p-2">
|
|
83
|
+
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
|
|
84
|
+
<Cable className="size-4" />
|
|
85
|
+
</div>
|
|
86
|
+
<div className="text-muted-foreground font-medium">Conectar propiedad</div>
|
|
87
|
+
</DropdownMenuItem>
|
|
88
|
+
</DropdownMenuContent>
|
|
89
|
+
</DropdownMenu>
|
|
90
|
+
</SidebarMenuItem>
|
|
91
|
+
</SidebarMenu>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import useSessionContext from "#cli/context/session/useSessionContext.mjs"
|
|
2
|
+
import IndexOffline from "#cli/pages/IndexOffline.jsx"
|
|
3
|
+
import IndexOnline from "#cli/pages/IndexOnline.jsx"
|
|
4
|
+
|
|
5
|
+
export default function Index() {
|
|
6
|
+
const { authenticated } = useSessionContext()
|
|
7
|
+
|
|
8
|
+
if (!authenticated) {
|
|
9
|
+
return <IndexOffline />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return <IndexOnline />
|
|
13
|
+
}
|