2023-10-23 23:33:39 +02:00
|
|
|
import Image from "next/image"
|
|
|
|
import useTranslation from "next-translate/useTranslation"
|
|
|
|
import { useState, useMemo } from "react"
|
|
|
|
import { Form, useForm } from "react-component-form"
|
|
|
|
import { EyeIcon, PhotographIcon } from "@heroicons/react/solid"
|
|
|
|
import { Type } from "@sinclair/typebox"
|
|
|
|
import axios from "axios"
|
|
|
|
import Link from "next/link"
|
|
|
|
import type { HandleUseFormCallback } from "react-component-form"
|
2022-01-14 23:15:51 +01:00
|
|
|
|
2023-10-23 23:33:39 +02:00
|
|
|
import { Input } from "../../design/Input"
|
|
|
|
import { Checkbox } from "../../design/Checkbox"
|
|
|
|
import { Textarea } from "../../design/Textarea"
|
|
|
|
import { SocialMediaButton } from "../../design/SocialMediaButton"
|
|
|
|
import { SwitchTheme } from "../../Header/SwitchTheme"
|
|
|
|
import { Language } from "../../Header/Language"
|
|
|
|
import { useAuthentication } from "../../../tools/authentication"
|
|
|
|
import { Button } from "../../design/Button"
|
|
|
|
import { FormState } from "../../design/FormState"
|
|
|
|
import { userSchema } from "../../../models/User"
|
|
|
|
import { userSettingsSchema } from "../../../models/UserSettings"
|
|
|
|
import type { ProviderOAuth } from "../../../models/OAuth"
|
|
|
|
import { providers } from "../../../models/OAuth"
|
|
|
|
import { useFormTranslation } from "../../../hooks/useFormTranslation"
|
2022-08-28 18:26:56 +02:00
|
|
|
|
|
|
|
const schema = {
|
|
|
|
name: userSchema.name,
|
|
|
|
status: Type.Optional(userSchema.status),
|
|
|
|
email: Type.Optional(Type.Union([userSchema.email, Type.Null()])),
|
|
|
|
website: Type.Optional(userSchema.website),
|
|
|
|
biography: Type.Optional(userSchema.biography),
|
|
|
|
isPublicGuilds: userSettingsSchema.isPublicGuilds,
|
2023-10-23 23:33:39 +02:00
|
|
|
isPublicEmail: userSettingsSchema.isPublicEmail,
|
2022-08-28 18:26:56 +02:00
|
|
|
}
|
2022-01-14 23:15:51 +01:00
|
|
|
|
2022-02-19 23:20:33 +01:00
|
|
|
export const UserSettings: React.FC = () => {
|
|
|
|
const { user, setUser, authentication } = useAuthentication()
|
2022-01-14 23:15:51 +01:00
|
|
|
const { t } = useTranslation()
|
2022-02-19 23:20:33 +01:00
|
|
|
const [inputValues, setInputValues] = useState({
|
|
|
|
name: user.name,
|
|
|
|
status: user.status,
|
|
|
|
email: user.email,
|
|
|
|
website: user.website,
|
|
|
|
biography: user.biography,
|
|
|
|
isPublicGuilds: user.settings.isPublicGuilds,
|
2023-10-23 23:33:39 +02:00
|
|
|
isPublicEmail: user.settings.isPublicEmail,
|
2022-02-19 23:20:33 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
const {
|
2022-08-28 18:26:56 +02:00
|
|
|
handleUseForm,
|
2022-02-19 23:20:33 +01:00
|
|
|
fetchState,
|
|
|
|
setFetchState,
|
|
|
|
message,
|
2022-08-28 18:26:56 +02:00
|
|
|
setMessage,
|
2023-10-23 23:33:39 +02:00
|
|
|
errors,
|
2022-08-28 18:26:56 +02:00
|
|
|
} = useForm(schema)
|
|
|
|
const { getFirstErrorTranslation } = useFormTranslation()
|
2022-02-19 23:20:33 +01:00
|
|
|
|
2022-04-08 20:59:04 +02:00
|
|
|
const hasAllProviders = useMemo(() => {
|
2022-08-31 21:44:33 +02:00
|
|
|
return providers.every((provider) => {
|
|
|
|
return user.strategies.includes(provider)
|
|
|
|
})
|
2022-04-08 20:59:04 +02:00
|
|
|
}, [user.strategies])
|
|
|
|
|
2022-08-28 18:26:56 +02:00
|
|
|
const onSubmit: HandleUseFormCallback<typeof schema> = async (formData) => {
|
2022-02-19 23:20:33 +01:00
|
|
|
try {
|
|
|
|
const { isPublicGuilds, isPublicEmail, ...userData } = formData
|
|
|
|
const userSettings = { isPublicEmail, isPublicGuilds }
|
|
|
|
const { data: userCurrentData } = await authentication.api.put(
|
|
|
|
`/users/current?redirectURI=${window.location.origin}/authentication/signin`,
|
2023-10-23 23:33:39 +02:00
|
|
|
userData,
|
2022-02-19 23:20:33 +01:00
|
|
|
)
|
2022-08-28 18:26:56 +02:00
|
|
|
setInputValues(formData as unknown as any)
|
2022-04-07 16:54:05 +02:00
|
|
|
const hasEmailChanged = user.email !== userCurrentData.user.email
|
|
|
|
if (hasEmailChanged) {
|
|
|
|
return {
|
2023-10-23 23:33:39 +02:00
|
|
|
type: "success",
|
|
|
|
message: "application:success-email-changed",
|
2022-04-07 16:54:05 +02:00
|
|
|
}
|
|
|
|
}
|
2022-02-19 23:20:33 +01:00
|
|
|
const { data: userCurrentSettings } = await authentication.api.put(
|
2023-10-23 23:33:39 +02:00
|
|
|
"/users/current/settings",
|
|
|
|
userSettings,
|
2022-02-19 23:20:33 +01:00
|
|
|
)
|
|
|
|
setUser((oldUser) => {
|
|
|
|
return {
|
|
|
|
...oldUser,
|
|
|
|
...userCurrentData,
|
2022-04-09 01:03:15 +02:00
|
|
|
settings: {
|
|
|
|
...oldUser.settings,
|
2023-10-23 23:33:39 +02:00
|
|
|
...userCurrentSettings.settings,
|
|
|
|
},
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
return {
|
2023-10-23 23:33:39 +02:00
|
|
|
type: "success",
|
|
|
|
message: "application:saved-information",
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
|
|
|
const message = error.response.data.message as string
|
2023-10-23 23:33:39 +02:00
|
|
|
if (message.endsWith("already taken.")) {
|
2022-02-19 23:20:33 +01:00
|
|
|
return {
|
2023-10-23 23:33:39 +02:00
|
|
|
type: "error",
|
|
|
|
message: "authentication:already-used",
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
2023-10-23 23:33:39 +02:00
|
|
|
} else if (message.endsWith("email to sign in.")) {
|
2022-02-19 23:20:33 +01:00
|
|
|
return {
|
2023-10-23 23:33:39 +02:00
|
|
|
type: "error",
|
|
|
|
message: "authentication:email-required-to-sign-in",
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
2023-10-23 23:33:39 +02:00
|
|
|
type: "error",
|
|
|
|
message: "errors:server-error",
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
2023-10-23 23:33:39 +02:00
|
|
|
type: "error",
|
|
|
|
message: "errors:server-error",
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const onChange: React.ChangeEventHandler<
|
|
|
|
HTMLInputElement | HTMLTextAreaElement
|
|
|
|
> = (event) => {
|
|
|
|
setInputValues((oldInputValues) => {
|
|
|
|
return {
|
|
|
|
...oldInputValues,
|
2023-10-23 23:33:39 +02:00
|
|
|
[event.target.name]: event.target.value,
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const onChangeCheckbox: React.ChangeEventHandler<HTMLInputElement> = (
|
2023-10-23 23:33:39 +02:00
|
|
|
event,
|
2022-02-19 23:20:33 +01:00
|
|
|
) => {
|
|
|
|
setInputValues((oldInputValues) => {
|
|
|
|
return {
|
|
|
|
...oldInputValues,
|
2023-10-23 23:33:39 +02:00
|
|
|
[event.target.name]: event.target.checked,
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
|
2023-10-23 23:33:39 +02:00
|
|
|
event,
|
2022-02-19 23:20:33 +01:00
|
|
|
) => {
|
2023-10-23 23:33:39 +02:00
|
|
|
setFetchState("loading")
|
2022-02-19 23:20:33 +01:00
|
|
|
const files = event?.target?.files
|
2023-01-11 17:39:09 +01:00
|
|
|
if (files != null && files.length === 1 && files[0] != null) {
|
2022-02-19 23:20:33 +01:00
|
|
|
const file = files[0]
|
|
|
|
const formData = new FormData()
|
2023-10-23 23:33:39 +02:00
|
|
|
formData.append("logo", file)
|
2022-02-19 23:20:33 +01:00
|
|
|
try {
|
|
|
|
const { data } = await authentication.api.put(
|
|
|
|
`/users/current/logo`,
|
2023-07-22 16:34:23 +02:00
|
|
|
formData,
|
|
|
|
{
|
|
|
|
headers: {
|
2023-10-23 23:33:39 +02:00
|
|
|
"Content-Type": "multipart/form-data",
|
|
|
|
},
|
|
|
|
},
|
2022-02-19 23:20:33 +01:00
|
|
|
)
|
|
|
|
setUser((oldUser) => {
|
|
|
|
return {
|
|
|
|
...oldUser,
|
2023-10-23 23:33:39 +02:00
|
|
|
logo: data.user.logo,
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
|
|
|
})
|
2023-10-23 23:33:39 +02:00
|
|
|
setFetchState("idle")
|
2022-02-19 23:20:33 +01:00
|
|
|
} catch (error) {
|
2023-10-23 23:33:39 +02:00
|
|
|
setFetchState("error")
|
|
|
|
setMessage("errors:server-error")
|
2022-02-19 23:20:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-01-14 23:15:51 +01:00
|
|
|
|
2022-03-16 14:04:19 +01:00
|
|
|
const handleSignout = async (): Promise<void> => {
|
2023-10-23 23:33:39 +02:00
|
|
|
setFetchState("loading")
|
2022-08-29 20:15:47 +02:00
|
|
|
await authentication.signoutServerSide()
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleSignoutAllDevices = async (): Promise<void> => {
|
2023-10-23 23:33:39 +02:00
|
|
|
setFetchState("loading")
|
2022-08-29 20:15:47 +02:00
|
|
|
await authentication.signoutAllDevicesServerSide()
|
2022-03-16 14:04:19 +01:00
|
|
|
}
|
|
|
|
|
2022-04-08 20:59:04 +02:00
|
|
|
const handleDeletionProvider = (
|
2023-10-23 23:33:39 +02:00
|
|
|
provider: ProviderOAuth,
|
2022-04-08 20:59:04 +02:00
|
|
|
): (() => Promise<void>) => {
|
|
|
|
return async () => {
|
|
|
|
try {
|
2023-10-23 23:33:39 +02:00
|
|
|
setFetchState("loading")
|
2022-04-08 20:59:04 +02:00
|
|
|
await authentication.api.delete(`/users/oauth2/${provider}`)
|
|
|
|
setUser((oldUser) => {
|
|
|
|
return {
|
|
|
|
...oldUser,
|
2022-08-31 21:44:33 +02:00
|
|
|
strategies: oldUser.strategies.filter((strategy) => {
|
|
|
|
return strategy !== provider
|
2023-10-23 23:33:39 +02:00
|
|
|
}),
|
2022-04-08 20:59:04 +02:00
|
|
|
}
|
|
|
|
})
|
2023-10-23 23:33:39 +02:00
|
|
|
setMessage("application:success-deleted-provider")
|
2022-04-08 20:59:04 +02:00
|
|
|
} catch (error) {
|
2023-10-23 23:33:39 +02:00
|
|
|
setFetchState("error")
|
|
|
|
setMessage("errors:server-error")
|
2022-04-08 20:59:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleAddProvider = (
|
2023-10-23 23:33:39 +02:00
|
|
|
provider: ProviderOAuth,
|
2022-04-08 20:59:04 +02:00
|
|
|
): (() => Promise<void>) => {
|
|
|
|
return async () => {
|
2023-10-23 23:33:39 +02:00
|
|
|
const redirect = window.location.href.replace(location.search, "")
|
2022-04-08 20:59:04 +02:00
|
|
|
const { data: url } = await authentication.api.get(
|
2023-10-23 23:33:39 +02:00
|
|
|
`/users/oauth2/${provider.toLowerCase()}/add-strategy?redirectURI=${redirect}`,
|
2022-04-08 20:59:04 +02:00
|
|
|
)
|
|
|
|
window.location.href = url
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-14 23:15:51 +01:00
|
|
|
return (
|
2022-02-19 23:20:33 +01:00
|
|
|
<Form
|
2022-08-28 18:26:56 +02:00
|
|
|
onSubmit={handleUseForm(onSubmit)}
|
2023-10-23 23:33:39 +02:00
|
|
|
className="my-auto flex flex-col items-center justify-center py-12 lg:min-w-[875px]"
|
2022-02-19 23:20:33 +01:00
|
|
|
>
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row">
|
|
|
|
<div className=" flex w-full flex-wrap items-center justify-center px-6 sm:w-max">
|
|
|
|
<div className="relative">
|
|
|
|
<div className="absolute z-50 h-full w-full">
|
|
|
|
<button className="relative flex h-full w-full items-center justify-center transition hover:scale-110">
|
2022-01-14 23:15:51 +01:00
|
|
|
<input
|
2023-10-23 23:33:39 +02:00
|
|
|
type="file"
|
|
|
|
className="absolute h-full w-full cursor-pointer opacity-0"
|
2022-02-19 23:20:33 +01:00
|
|
|
onChange={handleFileChange}
|
2022-01-14 23:15:51 +01:00
|
|
|
/>
|
2023-10-23 23:33:39 +02:00
|
|
|
<PhotographIcon color="white" className="h-8 w-8" />
|
2022-01-14 23:15:51 +01:00
|
|
|
</button>
|
|
|
|
</div>
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="flex items-center justify-center rounded-full bg-black shadow-xl">
|
2022-01-14 23:15:51 +01:00
|
|
|
<Image
|
2022-04-07 16:54:05 +02:00
|
|
|
quality={100}
|
2023-10-23 23:33:39 +02:00
|
|
|
className="rounded-full opacity-50"
|
2022-01-14 23:15:51 +01:00
|
|
|
src={
|
|
|
|
user.logo != null
|
2022-04-08 20:59:04 +02:00
|
|
|
? user.logo
|
2023-10-23 23:33:39 +02:00
|
|
|
: "/images/data/user-default.png"
|
2022-01-14 23:15:51 +01:00
|
|
|
}
|
2023-10-23 23:33:39 +02:00
|
|
|
alt="Profil Picture"
|
|
|
|
draggable="false"
|
2022-01-14 23:15:51 +01:00
|
|
|
height={125}
|
|
|
|
width={125}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="mx-12 flex flex-col">
|
2022-01-14 23:15:51 +01:00
|
|
|
<Input
|
2023-10-23 23:33:39 +02:00
|
|
|
name="name"
|
|
|
|
label={t("common:name")}
|
|
|
|
placeholder={t("common:name")}
|
|
|
|
className="!mt-0"
|
2022-02-19 23:20:33 +01:00
|
|
|
onChange={onChange}
|
2023-10-23 23:33:39 +02:00
|
|
|
value={inputValues.name ?? ""}
|
2022-08-28 18:26:56 +02:00
|
|
|
error={getFirstErrorTranslation(errors.name)}
|
2022-01-14 23:15:51 +01:00
|
|
|
/>
|
|
|
|
<Input
|
2023-10-23 23:33:39 +02:00
|
|
|
name="status"
|
|
|
|
label={t("application:status")}
|
|
|
|
placeholder={t("application:status")}
|
|
|
|
className="!mt-4"
|
2022-02-19 23:20:33 +01:00
|
|
|
onChange={onChange}
|
2023-10-23 23:33:39 +02:00
|
|
|
value={inputValues.status ?? ""}
|
2022-08-28 18:26:56 +02:00
|
|
|
error={getFirstErrorTranslation(errors.status)}
|
2022-01-14 23:15:51 +01:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="mt-12 flex w-full flex-col items-center justify-between sm:w-fit lg:flex-row">
|
|
|
|
<div className="w-4/5 pr-0 sm:w-[450px] lg:border-r-[1px] lg:border-neutral-700 lg:pr-12">
|
2022-02-19 23:20:33 +01:00
|
|
|
<Input
|
2023-10-23 23:33:39 +02:00
|
|
|
name="email"
|
|
|
|
label="Email"
|
|
|
|
placeholder="Email"
|
2022-02-19 23:20:33 +01:00
|
|
|
onChange={onChange}
|
2023-10-23 23:33:39 +02:00
|
|
|
value={inputValues.email ?? ""}
|
2022-08-28 18:26:56 +02:00
|
|
|
error={getFirstErrorTranslation(errors.email)}
|
2022-02-19 23:20:33 +01:00
|
|
|
/>
|
2022-01-14 23:15:51 +01:00
|
|
|
<Checkbox
|
2023-10-23 23:33:39 +02:00
|
|
|
name="isPublicEmail"
|
|
|
|
label={t("application:label-checkbox-email")}
|
|
|
|
id="checkbox-email-visibility"
|
2022-02-19 23:20:33 +01:00
|
|
|
onChange={onChangeCheckbox}
|
|
|
|
checked={inputValues.isPublicEmail}
|
2022-01-14 23:15:51 +01:00
|
|
|
/>
|
|
|
|
<Input
|
2023-10-23 23:33:39 +02:00
|
|
|
name="website"
|
|
|
|
label={t("application:website")}
|
|
|
|
placeholder={t("application:website")}
|
2022-02-19 23:20:33 +01:00
|
|
|
onChange={onChange}
|
2023-10-23 23:33:39 +02:00
|
|
|
value={inputValues.website ?? ""}
|
2022-08-28 18:26:56 +02:00
|
|
|
error={getFirstErrorTranslation(errors.website)}
|
2022-01-14 23:15:51 +01:00
|
|
|
/>
|
|
|
|
<Textarea
|
2023-10-23 23:33:39 +02:00
|
|
|
name="biography"
|
|
|
|
label={t("application:biography")}
|
|
|
|
placeholder={t("application:biography")}
|
|
|
|
id="textarea-biography"
|
2022-02-19 23:20:33 +01:00
|
|
|
onChange={onChange}
|
2023-10-23 23:33:39 +02:00
|
|
|
value={inputValues.biography ?? ""}
|
2022-01-14 23:15:51 +01:00
|
|
|
/>
|
|
|
|
</div>
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="flex h-full w-4/5 flex-col items-center justify-between pr-0 sm:w-[415px] lg:pl-12">
|
|
|
|
<div className="flex w-full items-center pt-10 lg:pt-0">
|
|
|
|
<Language className="!top-12" />
|
|
|
|
<div className="ml-auto flex">
|
2022-04-07 16:54:05 +02:00
|
|
|
<SwitchTheme />
|
2022-12-13 11:38:07 +01:00
|
|
|
<Link
|
|
|
|
href={`/application/users/${user.id}`}
|
2023-10-23 23:33:39 +02:00
|
|
|
className="group ml-3 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-slate-200 transition-colors hover:bg-slate-300 dark:bg-slate-700 hover:dark:bg-slate-800"
|
|
|
|
title="Preview Public Profile"
|
2022-12-13 11:38:07 +01:00
|
|
|
>
|
|
|
|
<EyeIcon
|
|
|
|
height={20}
|
2023-10-23 23:33:39 +02:00
|
|
|
className="opacity-50 transition-opacity group-hover:opacity-100"
|
2022-12-13 11:38:07 +01:00
|
|
|
/>
|
2022-04-07 16:54:05 +02:00
|
|
|
</Link>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="mt-14 flex w-full flex-col gap-4">
|
2022-04-08 20:59:04 +02:00
|
|
|
{!hasAllProviders ? (
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="flex w-full flex-col gap-4">
|
|
|
|
<h3 className="text-center">
|
|
|
|
{t("application:signin-with-an-account")}
|
2022-04-08 20:59:04 +02:00
|
|
|
</h3>
|
|
|
|
{providers.map((provider, index) => {
|
|
|
|
if (!user.strategies.includes(provider)) {
|
|
|
|
return (
|
|
|
|
<SocialMediaButton
|
|
|
|
key={index}
|
|
|
|
socialMedia={provider}
|
2023-10-23 23:33:39 +02:00
|
|
|
className="w-full justify-center"
|
2022-04-08 20:59:04 +02:00
|
|
|
onClick={handleAddProvider(provider)}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
{user.strategies.length !== 1 && (
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="mt-4 flex w-full flex-col gap-4">
|
|
|
|
<h3 className="text-center">
|
|
|
|
{t("application:signout-with-an-account")}
|
2022-04-08 20:59:04 +02:00
|
|
|
</h3>
|
|
|
|
{providers.map((provider, index) => {
|
|
|
|
if (user.strategies.includes(provider)) {
|
|
|
|
return (
|
|
|
|
<SocialMediaButton
|
|
|
|
key={index}
|
|
|
|
socialMedia={provider}
|
2023-10-23 23:33:39 +02:00
|
|
|
className="w-full justify-center"
|
2022-04-08 20:59:04 +02:00
|
|
|
onClick={handleDeletionProvider(provider)}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
)}
|
2022-01-14 23:15:51 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-02-19 23:20:33 +01:00
|
|
|
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="mt-12 flex flex-col items-center justify-center sm:w-fit">
|
|
|
|
<div className="space-x-6">
|
|
|
|
<Button type="submit">{t("application:save")}</Button>
|
|
|
|
<Button type="button" color="red" onClick={handleSignout}>
|
|
|
|
{t("application:signout")}
|
2022-03-16 14:04:19 +01:00
|
|
|
</Button>
|
|
|
|
</div>
|
2023-10-23 23:33:39 +02:00
|
|
|
<div className="mt-4">
|
|
|
|
<Button type="button" color="red" onClick={handleSignoutAllDevices}>
|
|
|
|
{t("application:signout-all-devices")}
|
2022-08-29 20:15:47 +02:00
|
|
|
</Button>
|
|
|
|
</div>
|
2022-08-28 18:26:56 +02:00
|
|
|
<FormState
|
|
|
|
state={fetchState}
|
|
|
|
message={message != null ? t(message) : undefined}
|
|
|
|
/>
|
2022-02-19 23:20:33 +01:00
|
|
|
</div>
|
|
|
|
</Form>
|
2022-01-14 23:15:51 +01:00
|
|
|
)
|
|
|
|
}
|