2023-04-22 16:41:13 +02:00
|
|
|
// Dependencies
|
2023-04-28 21:23:06 +02:00
|
|
|
import { useState, useEffect, useContext } from 'react'
|
2023-04-22 16:41:13 +02:00
|
|
|
import { useTranslation } from 'next-i18next'
|
|
|
|
import { DateTime } from 'luxon'
|
|
|
|
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
|
|
|
// Hooks
|
|
|
|
import { useAccount } from 'shared/hooks/use-account.mjs'
|
|
|
|
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
|
|
|
import { useToast } from 'shared/hooks/use-toast.mjs'
|
|
|
|
import { useRouter } from 'next/router'
|
2023-04-28 21:23:06 +02:00
|
|
|
// Context
|
|
|
|
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
|
|
|
import { ModalContext } from 'shared/context/modal-context.mjs'
|
2023-04-22 16:41:13 +02:00
|
|
|
// Components
|
|
|
|
import { BackToAccountButton, Choice } from './shared.mjs'
|
|
|
|
import { Popout } from 'shared/components/popout.mjs'
|
|
|
|
import { WebLink } from 'shared/components/web-link.mjs'
|
|
|
|
import { CopyIcon } from 'shared/components/icons.mjs'
|
|
|
|
import { Collapse } from 'shared/components/collapse.mjs'
|
|
|
|
import { TrashIcon } from 'shared/components/icons.mjs'
|
|
|
|
import { LeftIcon } from 'shared/components/icons.mjs'
|
|
|
|
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
|
|
|
import Markdown from 'react-markdown'
|
|
|
|
import { Tab } from './bio.mjs'
|
|
|
|
|
|
|
|
export const ns = ['account', 'toast']
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
const NewSet = ({ t, account, setGenerate, oneAdded, backend, toast, standAlone = false }) => {
|
|
|
|
// Context
|
|
|
|
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
|
|
|
|
|
|
|
// Hooks
|
2023-04-22 16:41:13 +02:00
|
|
|
const router = useRouter()
|
2023-04-28 21:23:06 +02:00
|
|
|
|
|
|
|
// State
|
2023-04-22 16:41:13 +02:00
|
|
|
const [name, setName] = useState('')
|
|
|
|
const [notes, setNotes] = useState('')
|
|
|
|
const [set, setSet] = useState(false)
|
|
|
|
const [activeTab, setActiveTab] = useState('edit')
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// Helper method to create a new set
|
2023-04-22 16:41:13 +02:00
|
|
|
const createSet = async () => {
|
2023-04-28 21:23:06 +02:00
|
|
|
startLoading()
|
2023-04-22 16:41:13 +02:00
|
|
|
const result = await backend.createSet({
|
|
|
|
name,
|
|
|
|
})
|
|
|
|
if (result.success) {
|
|
|
|
toast.success(<span>{t('nailedIt')}</span>)
|
|
|
|
setSet(result.data.set)
|
|
|
|
oneAdded()
|
|
|
|
} else toast.for.backendError()
|
2023-04-28 21:23:06 +02:00
|
|
|
stopLoading()
|
2023-04-22 16:41:13 +02:00
|
|
|
}
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// Helper method to clear inputs
|
2023-04-22 16:41:13 +02:00
|
|
|
const clear = () => {
|
|
|
|
setSet(false)
|
|
|
|
setGenerate(false)
|
|
|
|
}
|
2023-04-28 21:23:06 +02:00
|
|
|
|
|
|
|
// Shared props for tabs
|
2023-04-22 16:41:13 +02:00
|
|
|
const tabProps = { activeTab, setActiveTab, t }
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<h2>{t('newSet')}</h2>
|
|
|
|
<h3>{t('name')}</h3>
|
|
|
|
<p>{t('setNameDesc')}</p>
|
|
|
|
<input
|
|
|
|
value={name}
|
|
|
|
onChange={(evt) => setName(evt.target.value)}
|
|
|
|
className="input w-full input-bordered flex flex-row"
|
|
|
|
type="text"
|
|
|
|
placeholder={'Georg Cantor'}
|
|
|
|
/>
|
|
|
|
{account.control > 2 ? (
|
|
|
|
<>
|
|
|
|
<h3>{t('notes')}</h3>
|
|
|
|
<p>{t('setNotesDesc')}</p>
|
|
|
|
<div className="tabs w-full">
|
|
|
|
<Tab id="edit" {...tabProps} />
|
|
|
|
<Tab id="preview" {...tabProps} />
|
|
|
|
</div>
|
|
|
|
<div className="flex flex-row items-center mt-4">
|
|
|
|
{activeTab === 'edit' ? (
|
|
|
|
<textarea
|
|
|
|
rows="5"
|
|
|
|
className="textarea textarea-bordered textarea-lg w-full"
|
|
|
|
placeholder={t('placeholder')}
|
|
|
|
onChange={(evt) => setNotes(evt.target.value)}
|
|
|
|
value={notes}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<div className="text-left px-4 border w-full">
|
|
|
|
<Markdown>{notes}</Markdown>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
) : null}
|
|
|
|
<div className="flex flex-row gap-2 items-center w-full my-8">
|
|
|
|
<button
|
|
|
|
className="btn btn-primary grow capitalize"
|
|
|
|
disabled={name.length < 1}
|
|
|
|
onClick={createSet}
|
|
|
|
>
|
|
|
|
{t('newSet')}
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
className="btn btn-primary btn-outline capitalize"
|
|
|
|
onClick={() => (standAlone ? router.push('/account/sets') : setGenerate(false))}
|
|
|
|
>
|
|
|
|
{t('cancel')}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
const MeasurementsSet = ({ apikey, t, account, backend, oneAdded }) => {
|
|
|
|
// Context
|
|
|
|
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
|
|
|
const { setModal } = useContext(ModalContext)
|
|
|
|
|
|
|
|
// Hooks
|
2023-04-22 16:41:13 +02:00
|
|
|
const toast = useToast()
|
|
|
|
|
|
|
|
const fields = {
|
|
|
|
id: 'ID',
|
|
|
|
name: t('keyName'),
|
|
|
|
level: t('keyLevel'),
|
|
|
|
expiresAt: t('expires'),
|
|
|
|
createdAt: t('created'),
|
|
|
|
}
|
|
|
|
|
|
|
|
const expired = DateTime.fromISO(apikey.expiresAt).valueOf() < DateTime.now().valueOf()
|
|
|
|
|
|
|
|
const remove = async () => {
|
2023-04-28 21:23:06 +02:00
|
|
|
startLoading()
|
2023-04-22 16:41:13 +02:00
|
|
|
const result = await backend.removeApikey(apikey.id)
|
|
|
|
if (result) toast.success(t('gone'))
|
|
|
|
else toast.for.backendError()
|
|
|
|
// This just forces a refresh of the list from the server
|
|
|
|
// We obviously did not add a key here, but rather removed one
|
|
|
|
oneAdded()
|
2023-04-28 21:23:06 +02:00
|
|
|
stopLoading()
|
2023-04-22 16:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const removeModal = () => {
|
2023-04-28 21:23:06 +02:00
|
|
|
setModal(
|
|
|
|
<ModalWrapper slideFrom="top">
|
2023-04-22 16:41:13 +02:00
|
|
|
<h2>{t('areYouCertain')}</h2>
|
|
|
|
<p>{t('deleteKeyWarning')}</p>
|
|
|
|
<p className="flex flex-row gap-4 items-center justify-center">
|
|
|
|
<button className="btn btn-neutral btn-outline px-8">{t('cancel')}</button>
|
|
|
|
<button className="btn btn-error px-8" onClick={remove}>
|
|
|
|
{t('delete')}
|
|
|
|
</button>
|
|
|
|
</p>
|
|
|
|
</ModalWrapper>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const title = (
|
|
|
|
<div className="flex flex-row gap-2 items-center inline-block justify-around w-full">
|
|
|
|
<span>{apikey.name}</span>
|
|
|
|
<span className="font-normal">
|
|
|
|
{t('expires')}: <b>{DateTime.fromISO(apikey.expiresAt).toLocaleString()}</b>
|
|
|
|
</span>
|
|
|
|
<span className="opacity-50">|</span>
|
|
|
|
<span className="font-normal">
|
|
|
|
{t('keyLevel')}: <b>{apikey.level}</b>
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Collapse
|
|
|
|
title={[title, null]}
|
|
|
|
valid={!expired}
|
|
|
|
buttons={[
|
|
|
|
<button
|
|
|
|
key="button1"
|
|
|
|
className="z-10 btn btn-sm mr-4 bg-base-100 text-error hover:bg-error hover:text-error-content border-0"
|
|
|
|
onClick={account.control > 4 ? remove : removeModal}
|
|
|
|
>
|
|
|
|
<TrashIcon key="button2" />
|
|
|
|
</button>,
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
{expired ? (
|
|
|
|
<Popout warning compact>
|
|
|
|
<b>{t('keyExpired')}</b>
|
|
|
|
</Popout>
|
|
|
|
) : null}
|
|
|
|
{Object.entries(fields).map(([key, title]) => (
|
|
|
|
<Row title={title} key={key}>
|
|
|
|
{apikey[key]}
|
|
|
|
</Row>
|
|
|
|
))}
|
|
|
|
</Collapse>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Component for the 'new/apikey' page
|
|
|
|
//export const NewApikey = ({ app, standAlone = false }) => {
|
|
|
|
// const { account, token } = useAccount()
|
|
|
|
// const backend = useBackend(token)
|
|
|
|
// const { t } = useTranslation(ns)
|
|
|
|
// const toast = useToast()
|
|
|
|
//
|
|
|
|
// const [keys, setKeys] = useState([])
|
|
|
|
// const [generate, setGenerate] = useState(false)
|
|
|
|
// const [added, setAdded] = useState(0)
|
|
|
|
//
|
|
|
|
// const oneAdded = () => setAdded(added + 1)
|
|
|
|
//
|
|
|
|
// return (
|
|
|
|
// <div className="max-w-xl xl:pl-4">
|
|
|
|
// <NewKey {...{ app, t, account, setGenerate, backend, toast, oneAdded, standAlone }} />
|
|
|
|
// </div>
|
|
|
|
// )
|
|
|
|
//}
|
|
|
|
|
|
|
|
// Component for the account/sets page
|
2023-04-28 21:23:06 +02:00
|
|
|
export const Sets = () => {
|
|
|
|
// Context
|
|
|
|
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
|
|
|
|
|
|
|
// Hooks
|
2023-04-22 16:41:13 +02:00
|
|
|
const { account, token } = useAccount()
|
|
|
|
const backend = useBackend(token)
|
|
|
|
const { t } = useTranslation(ns)
|
|
|
|
const toast = useToast()
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// State
|
2023-04-22 16:41:13 +02:00
|
|
|
const [sets, setSets] = useState([])
|
|
|
|
const [generate, setGenerate] = useState(false)
|
|
|
|
const [added, setAdded] = useState(0)
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// Effects
|
2023-04-22 16:41:13 +02:00
|
|
|
useEffect(() => {
|
|
|
|
const getSets = async () => {
|
|
|
|
const result = await backend.getSets()
|
|
|
|
if (result.success) setSets(result.data.sets)
|
|
|
|
}
|
|
|
|
getSets()
|
|
|
|
}, [added])
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// Helper method to force a refresh
|
2023-04-22 16:41:13 +02:00
|
|
|
const oneAdded = () => setAdded(added + 1)
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="max-w-xl xl:pl-4">
|
|
|
|
{generate ? (
|
2023-04-28 21:23:06 +02:00
|
|
|
<NewSet {...{ t, account, setGenerate, backend, toast, oneAdded }} />
|
2023-04-22 16:41:13 +02:00
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<h2>{t('sets')}</h2>
|
|
|
|
{sets.map((set) => (
|
2023-04-28 21:23:06 +02:00
|
|
|
<Set {...{ account, apikey, t, backend, oneAdded }} key={apikey.id} />
|
2023-04-22 16:41:13 +02:00
|
|
|
))}
|
|
|
|
<button
|
|
|
|
className="btn btn-primary w-full capitalize mt-4"
|
|
|
|
onClick={() => setGenerate(true)}
|
|
|
|
>
|
|
|
|
{t('newSet')}
|
|
|
|
</button>
|
2023-04-28 21:23:06 +02:00
|
|
|
<BackToAccountButton loading={loading} />
|
2023-04-22 16:41:13 +02:00
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|