diff --git a/_frontend/src/components/Sidebar.jsx b/_frontend/src/components/Sidebar.jsx index 52cb932..d7ccd72 100644 --- a/_frontend/src/components/Sidebar.jsx +++ b/_frontend/src/components/Sidebar.jsx @@ -1,6 +1,8 @@ -import { useState } from 'preact/hooks' +import { useEffect, useState } from 'preact/hooks' import { Icon } from './Icon.jsx' import { useStore } from '../context.jsx' +import { useRemoteState } from '../util.jsx' +import { useToasts } from './Toasts.jsx' const SidebarDropdownItem = ({ icon, label, children }) => { const [collapsed, setCollapsed] = useState(false) @@ -28,7 +30,16 @@ const SidebarDropdownItem = ({ icon, label, children }) => { } export const Sidebar = () => { - const { sidebarVisibile, buckets, user } = useStore() + const { sidebarVisibile, user } = useStore() + + const [showToast] = useToasts() + + const [buckets, err] = useRemoteState('/api/buckets', []) + useEffect(() => { + if (err) { + showToast(`Errore "${err}"`) + } + }, [err]) return ( user === 'admin' && ( @@ -77,7 +88,7 @@ export const Sidebar = () => { {buckets.map(bucket => ( -
+
) diff --git a/_frontend/src/components/Toasts.jsx b/_frontend/src/components/Toasts.jsx index 3dd895b..1d14b13 100644 --- a/_frontend/src/components/Toasts.jsx +++ b/_frontend/src/components/Toasts.jsx @@ -1,35 +1,57 @@ -import { useStore } from '../context.jsx' +import { createContext } from 'preact' +import { useContext, useState } from 'preact/hooks' +import { Icon } from './Icon.jsx' let globalToastId = 0 -export const Toasts = () => { - const { toasts } = useStore() +const ToastContext = createContext([]) + +export const ToastProvider = ({ children }) => { + const [toasts, setToasts] = useState([]) + + const removeToast = uid => { + setToasts(toasts => toasts.filter(t => t.uid !== uid)) + } return ( -
- {toasts.map(({ i, removed, message }) => ( -
- {message} -
- ))} -
+ + {children} +
+ {toasts.map(({ uid, removed, message }) => ( +
+

{message}

+
removeToast(uid)}> + +
+
+ ))} +
+
) } -const cleanToasts = setToasts => { - setToasts(ts => ts.filter((t, i) => !t.removed)) -} +export const useToasts = () => { + const { setToasts } = useContext(ToastContext) + + const showToast = (message, { duration } = {}) => { + duration ??= 3000 + + const toast = { uid: globalToastId++, removed: false, message } + + setTimeout(() => { + setToasts(toasts => toasts.map(t => (t === toast ? { ...t, removed: true } : t))) + }, duration) -export const showToast = (_toasts, setToasts) => message => { - const toast = { i: globalToastId++, removed: false, message } + setTimeout(() => { + setToasts(toasts => toasts.filter(t => !t.removed)) + }, duration + 1000) - setTimeout(() => { - setToasts(ts => ts.map(t => (t === toast ? { ...t, removed: true } : t))) - }, 3000) + setToasts(ts => [...ts.slice(-5), toast]) + } - setTimeout(() => { - cleanToasts(setToasts) - }, 4000) + const clearToasts = () => { + setToasts([]) + } - setToasts(ts => [...ts.slice(-5), toast]) + return [showToast, clearToasts] } diff --git a/_frontend/src/main.jsx b/_frontend/src/main.jsx index 03a5f97..ea78197 100644 --- a/_frontend/src/main.jsx +++ b/_frontend/src/main.jsx @@ -7,9 +7,9 @@ import { Login } from './pages/Login.jsx' import { Store, useStore } from './context.jsx' import { changeCase, useRouter, useUser } from './util.jsx' import { Dashboard } from './pages/Dashboard.jsx' -import { showToast, Toasts } from './components/Toasts.jsx' import { Sidebar } from './components/Sidebar.jsx' import { MainContent } from './components/MainContent.jsx' +import { ToastProvider } from './components/Toasts.jsx' const App = () => { const [id, route, routeParams] = useRouter({ @@ -52,27 +52,14 @@ const App = () => { } }) - const [toasts, setToasts] = useState([]) const [sidebarVisibile, setSidebarVisible] = useState(true) - const [buckets, setBuckets] = useState([]) - useEffect(() => { - fetch('/api/buckets') - .then(res => res.json()) - .then(value => setBuckets(value)) - }, []) - const context = { // Auth user, // Sidebar sidebarVisibile, setSidebarVisible, - buckets, - // Toasts - toasts, - setToasts, - showToast: showToast(toasts, setToasts), // Routing route: { ...route, @@ -82,11 +69,12 @@ const App = () => { } return ( - - - - - + + + {user === 'admin' && } + + + ) } diff --git a/_frontend/src/pages/ApiKeys.jsx b/_frontend/src/pages/ApiKeys.jsx index 2b97b3e..994c26b 100644 --- a/_frontend/src/pages/ApiKeys.jsx +++ b/_frontend/src/pages/ApiKeys.jsx @@ -1,9 +1,10 @@ import { useEffect, useState } from 'preact/hooks' import { Icon } from '../components/Icon.jsx' +import { useToasts } from '../components/Toasts.jsx' import { useStore } from '../context.jsx' export const ApiKeys = () => { - const { showToast } = useStore() + const [showToast] = useToasts() const [keys, setKeys] = useState([]) @@ -40,7 +41,7 @@ export const ApiKeys = () => { await navigator.clipboard.writeText(key) showToast( <> - Copiata {key} negli appunti + Copiata chiave {key.substring(0, 6)}... negli appunti ) } @@ -62,7 +63,7 @@ export const ApiKeys = () => {
{/* Header */}
Chiave
-
Opzioni
+
Azioni
{/* Rows */} {keys.map(key => { return ( @@ -72,7 +73,7 @@ export const ApiKeys = () => { {key}
-
+
@@ -85,7 +100,7 @@ export const Buckets = () => {
-
+
Nuovo Bucket
diff --git a/_frontend/src/util.jsx b/_frontend/src/util.jsx index 2051cb2..84d49a5 100644 --- a/_frontend/src/util.jsx +++ b/_frontend/src/util.jsx @@ -58,22 +58,28 @@ export const useRouter = routes => { // useRemoteState export function useRemoteState(url, initialValue = null) { + const [error, setError] = useState(null) const [value, setValue] = useState(initialValue) - const updateValue = async () => { - const res = await fetch(url) - if (!res.ok) { - console.error(res.body) + const refresh = async () => { + try { + const res = await fetch(url) + if (!res.ok) { + setError(await res.text()) + return + } + + setValue(await res.json()) + } catch (err) { + setError(err) } - - setValue(await res.json()) } useEffect(() => { - updateValue() + refresh() }, []) - return [value, updateValue] + return [value, error, refresh] } // diff --git a/_frontend/styles/main.scss b/_frontend/styles/main.scss index 226ed1d..26cd6ce 100644 --- a/_frontend/styles/main.scss +++ b/_frontend/styles/main.scss @@ -18,6 +18,7 @@ --fg-light-500: #333; --fg-dark-300: #505050; + --fg-dark-450: #c0c0c0; --fg-dark-500: #ddd; --ft-mono: 'JetBrains Mono', monospace; @@ -262,6 +263,10 @@ main { gap: 0.5rem; } + + .grow { + flex-grow: 1; + } } // @@ -269,6 +274,8 @@ main { // .toasts { + user-select: none; + position: absolute; z-index: 10; @@ -291,7 +298,7 @@ main { background: #38383d; color: var(--fg-dark-500); - padding: 0.5rem 1rem; + padding: 0.5rem 0.5rem 0.5rem 1rem; border-radius: 0.5rem; opacity: 0.95; @@ -299,6 +306,22 @@ main { animation: toast-fade-in 250ms ease-out; transition: opacity 250ms ease-in; + display: flex; + align-items: center; + + gap: 0.5rem; + + .toast-close { + display: flex; + align-items: center; + + cursor: pointer; + + &:hover { + color: var(--fg-dark-450); + } + } + &.removed { opacity: 0; } @@ -344,8 +367,6 @@ main { } & > div { - min-width: 4rem; - display: flex; align-items: center; } @@ -363,6 +384,8 @@ form { label { justify-self: end; + + font-weight: var(--ft-sans-wt-bold); } .fill { @@ -589,7 +612,7 @@ $heading-scale: 1.33; .route-api-keys { .table { - min-width: 30rem; + min-width: 35rem; .name { button { @@ -597,8 +620,9 @@ $heading-scale: 1.33; } } - .options { + .actions { display: flex; + align-items: center; justify-content: end; gap: 0.5rem; } @@ -620,6 +644,13 @@ $heading-scale: 1.33; width: 100%; } } + + .actions { + display: flex; + align-items: center; + justify-content: end; + gap: 0.5rem; + } } } } @@ -634,3 +665,120 @@ $heading-scale: 1.33; } } } + +// +// Mobile +// + +@media screen and (max-width: 512px) { + main { + .sidebar { + width: 0; + min-width: 0; + + &.collapsed { + // width: 100%; + min-width: calc(100vw - 3rem); + } + } + + .main-content { + & > .content { + max-width: 100%; + } + + .table { + min-width: unset; + max-width: 100%; + + .cells { + max-width: 100%; + gap: 0; + + & > div { + min-height: 2.25rem; + gap: 1rem; + padding: 0; + } + + .header { + display: none; + } + + display: flex; + flex-direction: row; + + flex-wrap: wrap; + } + } + } + } + + .panel { + max-width: 100%; + } + + .route-api-keys .table { + .cells { + & > div { + overflow-x: auto; + } + + .name { + &::before { + content: 'Nome: '; + font-weight: var(--ft-sans-wt-bold); + } + + button { + font-size: 90%; + word-wrap: break-word; + } + } + + .actions { + &::before { + content: 'Azioni: '; + font-weight: var(--ft-sans-wt-bold); + } + + margin-bottom: 1rem; + } + } + } + + .route-buckets .table .cells { + .name { + &::before { + content: 'Nome: '; + font-weight: var(--ft-sans-wt-bold); + } + } + .settings { + &::before { + content: 'Settings: '; + font-weight: var(--ft-sans-wt-bold); + } + + min-width: unset; + } + .actions { + width: 100%; + + &::before { + content: 'Azioni: '; + font-weight: var(--ft-sans-wt-bold); + } + + justify-content: start; + + &:not(:last-child) { + margin-bottom: 1rem; + } + } + } + + input { + max-width: 100%; + } +}