toasts
This commit is contained in:
parent
7fcbc4d98e
commit
9ee1f0bcbc
|
@ -9,6 +9,7 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/themes": "^2.0.3",
|
"@radix-ui/themes": "^2.0.3",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|
|
@ -5,6 +5,9 @@ settings:
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@radix-ui/react-toast':
|
||||||
|
specifier: ^1.1.5
|
||||||
|
version: 1.1.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/themes':
|
'@radix-ui/themes':
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
|
version: 2.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
@ -1240,6 +1243,38 @@ packages:
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-toast@1.1.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.8
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0)
|
||||||
|
'@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.48
|
||||||
|
'@types/react-dom': 18.2.18
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
|
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
|
@ -1,3 +1,97 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* mfw "ui library" doesnt ship styles */
|
||||||
|
.ToastViewport {
|
||||||
|
--viewport-padding: 25px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--viewport-padding);
|
||||||
|
gap: 10px;
|
||||||
|
width: 390px;
|
||||||
|
max-width: 100vw;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
z-index: 2147483647;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastRoot {
|
||||||
|
background-color: var(--color-panel-solid);
|
||||||
|
border: 1px solid var(--gray-4);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
|
||||||
|
padding: 15px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: 'title action' 'description action';
|
||||||
|
grid-template-columns: auto max-content;
|
||||||
|
column-gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-state='open'] {
|
||||||
|
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.ToastRoot[data-state='closed'] {
|
||||||
|
animation: hide 200ms ease-in;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='move'] {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='cancel'] {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 200ms ease-out;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='end'] {
|
||||||
|
animation: swipeOut 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hide {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swipeOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastTitle {
|
||||||
|
grid-area: title;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--slate-12);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastDescription {
|
||||||
|
grid-area: description;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--slate-11);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastAction {
|
||||||
|
grid-area: action;
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import AuthWrapper from '@/lib/components/wrapper/AuthWrapper';
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import NavigationWrapper from '@/lib/components/wrapper/NavigationWrapper';
|
import NavigationWrapper from '@/lib/components/wrapper/NavigationWrapper';
|
||||||
import BackgroundImage from '@/lib/components/ui/BackgroundImage';
|
import BackgroundImage from '@/lib/components/ui/BackgroundImage';
|
||||||
|
import ToastProvider from '@/lib/providers/ToastProvider';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Maddy Admin',
|
title: 'Maddy Admin',
|
||||||
|
@ -21,11 +22,13 @@ export default async function RootLayout({
|
||||||
<body className={`p-4 h-full m-0`}>
|
<body className={`p-4 h-full m-0`}>
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<BackgroundImage />
|
<BackgroundImage />
|
||||||
|
<ToastProvider>
|
||||||
<AuthWrapper session={await getServerSession()}>
|
<AuthWrapper session={await getServerSession()}>
|
||||||
<NavigationWrapper>
|
<NavigationWrapper>
|
||||||
{children}
|
{children}
|
||||||
</NavigationWrapper>
|
</NavigationWrapper>
|
||||||
</AuthWrapper>
|
</AuthWrapper>
|
||||||
|
</ToastProvider>
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -4,44 +4,18 @@ import ConnectionDetailsCard from "@/lib/components/ui/user/ConnectionDetailsCar
|
||||||
import OwnAliasesCard from "@/lib/components/ui/user/OwnAliasesCard";
|
import OwnAliasesCard from "@/lib/components/ui/user/OwnAliasesCard";
|
||||||
import OwnCredentialsCard from "@/lib/components/ui/user/OwnCredentialsCard";
|
import OwnCredentialsCard from "@/lib/components/ui/user/OwnCredentialsCard";
|
||||||
import useWindowDimensions from "@/lib/hooks/useWindowDimensions";
|
import useWindowDimensions from "@/lib/hooks/useWindowDimensions";
|
||||||
import { Callout, Grid, Heading } from "@radix-ui/themes";
|
import { Grid, Heading } from "@radix-ui/themes";
|
||||||
import { CheckIcon } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export default function SelfService() {
|
export default function SelfService() {
|
||||||
const [passwordChanged, setPasswordChanged] = useState(false);
|
|
||||||
const dimensions = useWindowDimensions();
|
const dimensions = useWindowDimensions();
|
||||||
const mobileUi = dimensions.width < 1200;
|
const mobileUi = dimensions.width < 1200;
|
||||||
|
|
||||||
function PasswordChangedAlert() {
|
|
||||||
const [hide, setHide] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timeout = setTimeout(() => {
|
|
||||||
setHide(true);
|
|
||||||
setTimeout(() => setPasswordChanged(false), 500);
|
|
||||||
}, 5000);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Callout.Root className={`transition-all ${hide ? "opacity-0 h-0 m-0 !p-0" : " mb-4"}`}>
|
|
||||||
<Callout.Icon><CheckIcon /></Callout.Icon>
|
|
||||||
<Callout.Text>Password updated!</Callout.Text>
|
|
||||||
</Callout.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{passwordChanged && (
|
|
||||||
<PasswordChangedAlert />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Heading className="pb-4">Account settings</Heading>
|
<Heading className="pb-4">Account settings</Heading>
|
||||||
|
|
||||||
<Grid display="inline-grid" columns={mobileUi ? "1" : "3"} gap="4" width={mobileUi ? "100%" : "auto"}>
|
<Grid display="inline-grid" columns={mobileUi ? "1" : "3"} gap="4" width={mobileUi ? "100%" : "auto"}>
|
||||||
<OwnCredentialsCard onPasswordChange={() => setPasswordChanged(true)} />
|
<OwnCredentialsCard />
|
||||||
<ConnectionDetailsCard />
|
<ConnectionDetailsCard />
|
||||||
<OwnAliasesCard />
|
<OwnAliasesCard />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { createAlias } from "@/lib/actions";
|
import { createAlias } from "@/lib/actions";
|
||||||
import { AliasEntry } from "@/lib/db";
|
import { AliasEntry } from "@/lib/db";
|
||||||
|
import { ToastContext } from "@/lib/providers/ToastProvider";
|
||||||
import { Button, Callout, Dialog, Flex, TextField } from "@radix-ui/themes";
|
import { Button, Callout, Dialog, Flex, TextField } from "@radix-ui/themes";
|
||||||
import { AtSignIcon, MailWarningIcon } from "lucide-react";
|
import { AtSignIcon, MailWarningIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
|
|
||||||
export default function CreateAliasButton({ user, onCreate }: { user: string, onCreate?: (alias: AliasEntry) => any }) {
|
export default function CreateAliasButton({ user, onCreate }: { user: string, onCreate?: (alias: AliasEntry) => any }) {
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
|
const toast = useContext(ToastContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root onOpenChange={(open) => !open && setValue("")}>
|
<Dialog.Root onOpenChange={(open) => !open && setValue("")}>
|
||||||
|
@ -45,9 +47,18 @@ export default function CreateAliasButton({ user, onCreate }: { user: string, on
|
||||||
try {
|
try {
|
||||||
const alias = await createAlias(user, value);
|
const alias = await createAlias(user, value);
|
||||||
onCreate?.(alias);
|
onCreate?.(alias);
|
||||||
|
toast({
|
||||||
|
title: "Alias created",
|
||||||
|
description: value,
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert(e);
|
toast({
|
||||||
|
title: "Failed to create alias",
|
||||||
|
description: `${e}`,
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>Create</Button>
|
>Create</Button>
|
||||||
|
|
|
@ -4,16 +4,18 @@ import { GRAVATAR_DEFAULT } from "@/lib/constants";
|
||||||
import { AliasEntry } from "@/lib/db";
|
import { AliasEntry } from "@/lib/db";
|
||||||
import { isAdmin, sha256sum } from "@/lib/util";
|
import { isAdmin, sha256sum } from "@/lib/util";
|
||||||
import { Avatar, Badge, Button, Card, Code, Dialog, Flex, Grid, Heading, Table, Text } from "@radix-ui/themes";
|
import { Avatar, Badge, Button, Card, Code, Dialog, Flex, Grid, Heading, Table, Text } from "@radix-ui/themes";
|
||||||
import { useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import GhostMessage from "../GhostMessage";
|
import GhostMessage from "../GhostMessage";
|
||||||
import { BananaIcon } from "lucide-react";
|
import { BananaIcon } from "lucide-react";
|
||||||
import LoadingSpinner from "../LoadingSpinner";
|
import LoadingSpinner from "../LoadingSpinner";
|
||||||
import { approveAlias, deleteAlias, fetchUserAliases } from "@/lib/actions";
|
import { approveAlias, deleteAlias, fetchUserAliases } from "@/lib/actions";
|
||||||
import GenericConfirmationDialog from "../GenericConfirmationDialog";
|
import GenericConfirmationDialog from "../GenericConfirmationDialog";
|
||||||
import CreateAliasButton from "./CreateAliasButton";
|
import CreateAliasButton from "./CreateAliasButton";
|
||||||
|
import { ToastContext } from "@/lib/providers/ToastProvider";
|
||||||
|
|
||||||
export default function ManageUserButton({ email }: { email: string }) {
|
export default function ManageUserButton({ email }: { email: string }) {
|
||||||
const [aliases, setAliases] = useState<AliasEntry[] | null>(null);
|
const [aliases, setAliases] = useState<AliasEntry[] | null>(null);
|
||||||
|
const toast = useContext(ToastContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUserAliases(email).then(setAliases);
|
fetchUserAliases(email).then(setAliases);
|
||||||
|
@ -77,8 +79,21 @@ export default function ManageUserButton({ email }: { email: string }) {
|
||||||
description={<>Do you want to approve <Code>{alias.address}</Code>'s alias request for <Code>{alias.alias}</Code>?</>}
|
description={<>Do you want to approve <Code>{alias.address}</Code>'s alias request for <Code>{alias.alias}</Code>?</>}
|
||||||
labelConfirm="Approve"
|
labelConfirm="Approve"
|
||||||
action={async () => {
|
action={async () => {
|
||||||
|
try {
|
||||||
await approveAlias(alias.alias);
|
await approveAlias(alias.alias);
|
||||||
setAliases(aliases.map((a) => a.id == alias.id ? { ...alias, pending: false } : a));
|
setAliases(aliases.map((a) => a.id == alias.id ? { ...alias, pending: false } : a));
|
||||||
|
toast({
|
||||||
|
title: "Alias approved",
|
||||||
|
description: alias.alias,
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
toast({
|
||||||
|
title: "Failed to approve alias",
|
||||||
|
description: `${e}`,
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button size="1" variant="surface">Approve</Button>
|
<Button size="1" variant="surface">Approve</Button>
|
||||||
|
@ -89,8 +104,21 @@ export default function ManageUserButton({ email }: { email: string }) {
|
||||||
description={<>Are you sure you want to delete <Code>{alias.alias}</Code>?</>}
|
description={<>Are you sure you want to delete <Code>{alias.alias}</Code>?</>}
|
||||||
labelConfirm="Delete"
|
labelConfirm="Delete"
|
||||||
action={async () => {
|
action={async () => {
|
||||||
|
try {
|
||||||
await deleteAlias(alias.alias);
|
await deleteAlias(alias.alias);
|
||||||
setAliases(aliases.filter((a) => a.id != alias.id));
|
setAliases(aliases.filter((a) => a.id != alias.id));
|
||||||
|
toast({
|
||||||
|
title: "Alias deleted",
|
||||||
|
description: alias.alias,
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
toast({
|
||||||
|
title: "Failed to delete alias",
|
||||||
|
description: `${e}`,
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button size="1" variant="soft">Delete</Button>
|
<Button size="1" variant="soft">Delete</Button>
|
||||||
|
|
|
@ -7,9 +7,10 @@ import { aliasesNeedApproval } from "@/lib/util";
|
||||||
import { Badge, Box, Button, Callout, Card, Code, Dialog, DropdownMenu, Flex, Heading, ScrollArea, Table, Text, TextField } from "@radix-ui/themes";
|
import { Badge, Box, Button, Callout, Card, Code, Dialog, DropdownMenu, Flex, Heading, ScrollArea, Table, Text, TextField } from "@radix-ui/themes";
|
||||||
import { ChevronDownIcon, InfoIcon, UsersRoundIcon } from "lucide-react";
|
import { ChevronDownIcon, InfoIcon, UsersRoundIcon } from "lucide-react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import LoadingSpinner from "../LoadingSpinner";
|
import LoadingSpinner from "../LoadingSpinner";
|
||||||
import GhostMessage from "../GhostMessage";
|
import GhostMessage from "../GhostMessage";
|
||||||
|
import { ToastContext } from "@/lib/providers/ToastProvider";
|
||||||
|
|
||||||
export default function OwnAliasesCard() {
|
export default function OwnAliasesCard() {
|
||||||
const session = useSession().data;
|
const session = useSession().data;
|
||||||
|
@ -17,6 +18,7 @@ export default function OwnAliasesCard() {
|
||||||
const [newAliasUsername, setNewAliasUsername] = useState("");
|
const [newAliasUsername, setNewAliasUsername] = useState("");
|
||||||
const [newAliasDomain, setNewAliasDomain] = useState(ALIAS_DOMAINS[0]);
|
const [newAliasDomain, setNewAliasDomain] = useState(ALIAS_DOMAINS[0]);
|
||||||
const [allowAlias, setAllowAlias] = useState(false);
|
const [allowAlias, setAllowAlias] = useState(false);
|
||||||
|
const toast = useContext(ToastContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchOwnAliases().then(setAliases);
|
fetchOwnAliases().then(setAliases);
|
||||||
|
@ -98,9 +100,18 @@ export default function OwnAliasesCard() {
|
||||||
try {
|
try {
|
||||||
const alias = await createAliasSelf(`${newAliasUsername}@${newAliasDomain}`);
|
const alias = await createAliasSelf(`${newAliasUsername}@${newAliasDomain}`);
|
||||||
setAliases([...(aliases ?? []), alias]);
|
setAliases([...(aliases ?? []), alias]);
|
||||||
|
toast({
|
||||||
|
title: "Alias created",
|
||||||
|
description: alias.pending ? "Administrator approval pending" : undefined,
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert(e);
|
toast({
|
||||||
|
title: "Failed to create alias",
|
||||||
|
description: `${e}`,
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}>Create</Button>
|
}}>Create</Button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
|
@ -153,9 +164,18 @@ export default function OwnAliasesCard() {
|
||||||
try {
|
try {
|
||||||
await deleteAlias(alias.alias);
|
await deleteAlias(alias.alias);
|
||||||
setAliases(aliases.filter((a) => a.id != alias.id));
|
setAliases(aliases.filter((a) => a.id != alias.id));
|
||||||
|
toast({
|
||||||
|
title: "Alias deleted",
|
||||||
|
description: alias.alias,
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert(e);
|
toast({
|
||||||
|
title: "Failed to delete alias",
|
||||||
|
description: `${e}`,
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { changeOwnPassword } from "@/lib/actions";
|
import { changeOwnPassword } from "@/lib/actions";
|
||||||
|
import { ToastContext } from "@/lib/providers/ToastProvider";
|
||||||
import { Box, Button, Callout, Card, Dialog, Flex, Heading, IconButton, Link, Popover, Text, TextField, Tooltip } from "@radix-ui/themes";
|
import { Box, Button, Callout, Card, Dialog, Flex, Heading, IconButton, Link, Popover, Text, TextField, Tooltip } from "@radix-ui/themes";
|
||||||
import { AlertCircleIcon, InfoIcon, LockIcon, UserIcon } from "lucide-react";
|
import { AlertCircleIcon, InfoIcon, LockIcon, UserIcon } from "lucide-react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
|
|
||||||
export default function OwnCredentialsCard({ onPasswordChange }: { onPasswordChange?: () => any }) {
|
export default function OwnCredentialsCard() {
|
||||||
const session = useSession().data;
|
const session = useSession().data;
|
||||||
const [newPassword, setNewPassword] = useState("");
|
const [newPassword, setNewPassword] = useState("");
|
||||||
const validPassword = newPassword.length >= 8;
|
const validPassword = newPassword.length >= 8;
|
||||||
|
const toast = useContext(ToastContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
|
@ -91,9 +93,18 @@ export default function OwnCredentialsCard({ onPasswordChange }: { onPasswordCha
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await changeOwnPassword(newPassword);
|
await changeOwnPassword(newPassword);
|
||||||
onPasswordChange?.();
|
toast({
|
||||||
|
title: "Password changed",
|
||||||
|
description: "balls",
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e);
|
console.error(e);
|
||||||
|
toast({
|
||||||
|
title: "Password change failed",
|
||||||
|
description: `${e}`,
|
||||||
|
variant: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>Update</Button>
|
>Update</Button>
|
||||||
|
|
80
src/lib/providers/ToastProvider.tsx
Normal file
80
src/lib/providers/ToastProvider.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
|
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||||
|
import { CheckIcon, CircleSlashIcon, XCircleIcon, XIcon } from "lucide-react";
|
||||||
|
import { createContext, useState } from "react";
|
||||||
|
|
||||||
|
export type ToastData = {
|
||||||
|
title: string,
|
||||||
|
description?: string,
|
||||||
|
variant?: 'normal' | 'error' | 'success',
|
||||||
|
};
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
return `${Math.random()}${Date.now()}`; // good enough i guess
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToastDataInternal = ToastData & { id: string };
|
||||||
|
|
||||||
|
export const ToastContext = createContext((toast: ToastData) => { });
|
||||||
|
|
||||||
|
export default function ToastProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const [toasts, setToasts] = useState<ToastDataInternal[]>([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Provider>
|
||||||
|
{toasts.map((toast) => (
|
||||||
|
<Toast
|
||||||
|
key={toast.id}
|
||||||
|
toast={toast}
|
||||||
|
onClosed={() => setToasts(toasts.filter((t) => t.id != toast.id))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<ToastContext.Provider value={(toast: ToastData) => {
|
||||||
|
// Updating the "local" `toasts` variable before calling `setToasts()` allows us to
|
||||||
|
// spawn multiple toasts without re-rendering. Otherwise, calling `toast()` twice
|
||||||
|
// will only display the last toast.
|
||||||
|
toasts.push({ ...toast, id: genId() });
|
||||||
|
setToasts([...toasts]);
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</ToastContext.Provider>
|
||||||
|
<ToastPrimitives.Viewport className="ToastViewport" />
|
||||||
|
</ToastPrimitives.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toast({ toast, onClosed }: { toast: ToastData, onClosed: () => void }) {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
className={`ToastRoot`}
|
||||||
|
duration={3000}
|
||||||
|
onOpenChange={(state) => !state && onClosed()}
|
||||||
|
>
|
||||||
|
<Flex justify="between" align="center" gap="4">
|
||||||
|
<Text>
|
||||||
|
{
|
||||||
|
toast.variant == "error"
|
||||||
|
? <CircleSlashIcon />
|
||||||
|
: toast.variant == "success"
|
||||||
|
? <CheckIcon />
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
<Box grow="1">
|
||||||
|
<ToastPrimitives.Title className="ToastTitle">{toast.title}</ToastPrimitives.Title>
|
||||||
|
{toast.description && <ToastPrimitives.Description className="ToastDescription">{toast.description}</ToastPrimitives.Description>}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
<ToastPrimitives.Close className="bg-transparent border-none p-0">
|
||||||
|
<Text color="gray"><XIcon size="16" /></Text>
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
</ToastPrimitives.Root>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue