Message flash provider

Allows errors to be displayed neatly in ephemeral boxes at bottom of screen
This commit is contained in:
jude 2023-11-05 13:45:04 +00:00
parent f310bbef54
commit 30dfaa17af
11 changed files with 131 additions and 31 deletions

View File

@ -9,8 +9,9 @@ type UserInfo = {
}; };
export type GuildInfo = { export type GuildInfo = {
id: string; patreon: boolean;
name: string; name: string;
error?: string;
}; };
type EmbedField = { type EmbedField = {
@ -95,6 +96,13 @@ export const fetchUserGuilds = () => ({
staleTime: USER_INFO_STALE_TIME, staleTime: USER_INFO_STALE_TIME,
}); });
export const fetchGuildInfo = (guild: string) => ({
queryKey: ["GUILD_INFO", guild],
queryFn: () =>
axios.get(`/dashboard/api/guild/${guild}`).then((resp) => resp.data) as Promise<GuildInfo>,
staleTime: GUILD_INFO_STALE_TIME,
});
export const fetchGuildChannels = (guild: string) => ({ export const fetchGuildChannels = (guild: string) => ({
queryKey: ["GUILD_CHANNELS", guild], queryKey: ["GUILD_CHANNELS", guild],
queryFn: () => queryFn: () =>
@ -142,10 +150,12 @@ export const patchGuildReminder = (guild: string) => ({
export const postGuildReminder = (guild: string) => ({ export const postGuildReminder = (guild: string) => ({
mutationFn: (reminder: Reminder) => mutationFn: (reminder: Reminder) =>
axios.post(`/dashboard/api/guild/${guild}/reminders`, { axios
...reminder, .post(`/dashboard/api/guild/${guild}/reminders`, {
utc_time: reminder.utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"), ...reminder,
}), utc_time: reminder.utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"),
})
.then((resp) => resp.data),
}); });
export const deleteGuildReminder = (guild: string) => ({ export const deleteGuildReminder = (guild: string) => ({

View File

@ -0,0 +1,6 @@
import { createContext } from "preact";
import { useContext } from "preact/compat";
export const FlashContext = createContext(null);
export const useFlash = () => useContext(FlashContext);

View File

@ -0,0 +1,30 @@
import { FlashContext } from "./FlashContext";
import { useState } from "preact/hooks";
import { MESSAGE_FLASH_TIME } from "../../consts";
export const FlashProvider = ({ children }) => {
const [messages, setMessages] = useState([] as string[]);
return (
<FlashContext.Provider
value={(message: string) => {
setMessages((messages: string[]) => [...messages, message]);
setTimeout(() => {
setMessages((messages) => [...messages].splice(1));
}, MESSAGE_FLASH_TIME);
}}
>
<>
{children}
{messages.map((message) => (
<div class="notification is-danger flash-message is-active">
<span class="icon">
<i class="far fa-exclamation-circle"></i>
</span>{" "}
<span class="error-message">{message}</span>
</div>
))}
</>
</FlashContext.Provider>
);
};

View File

@ -2,31 +2,29 @@ import { Sidebar } from "../Sidebar";
import { QueryClient, QueryClientProvider } from "react-query"; import { QueryClient, QueryClientProvider } from "react-query";
import { Route, Router, Switch } from "wouter"; import { Route, Router, Switch } from "wouter";
import { Welcome } from "../Welcome"; import { Welcome } from "../Welcome";
import { GuildReminders } from "../GuildReminders"; import { Guild } from "../Guild";
import { FlashProvider } from "./FlashProvider";
export function App() { export function App() {
const queryClient = new QueryClient(); const queryClient = new QueryClient();
return ( return (
<QueryClientProvider client={queryClient}> <FlashProvider>
<> <QueryClientProvider client={queryClient}>
<Router base={"/dashboard"}> <Router base={"/dashboard"}>
<div class="columns is-gapless dashboard-frame"> <div class="columns is-gapless dashboard-frame">
<Sidebar></Sidebar> <Sidebar></Sidebar>
<div class="column is-main-content"> <div class="column is-main-content">
<Switch> <Switch>
<Route <Route path={"/:guild/reminders"} component={Guild}></Route>
path={"/:guild/reminders"}
component={GuildReminders}
></Route>
<Route> <Route>
<Welcome></Welcome> <Welcome />
</Route> </Route>
</Switch> </Switch>
</div> </div>
</div> </div>
</Router> </Router>
</> </QueryClientProvider>
</QueryClientProvider> </FlashProvider>
); );
} }

View File

@ -0,0 +1,25 @@
export const GuildError = () => {
return (
<div class="hero is-fullheight">
<div class="hero-body">
<div class="container has-text-centered">
<p class="title">We couldn't get this server's data</p>
<p class="subtitle">
Please check Reminder Bot is in the server, and has correct permissions.
</p>
<a
class="button is-size-4 is-rounded is-success"
href="https://invite.reminder-bot.com"
>
<p class="is-size-4">
<span>Add to Server</span>{" "}
<span class="icon">
<i class="fas fa-chevron-right"></i>
</span>
</p>
</a>
</div>
</div>
</div>
);
};

View File

@ -1,14 +1,13 @@
import { useParams } from "wouter"; import { useParams } from "wouter";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { fetchGuildReminders } from "../../api"; import { fetchGuildReminders } from "../../api";
import { QueryKeys } from "../../consts";
import { EditReminder } from "../Reminder/EditReminder"; import { EditReminder } from "../Reminder/EditReminder";
import { CreateReminder } from "../Reminder/CreateReminder"; import { CreateReminder } from "../Reminder/CreateReminder";
export const GuildReminders = () => { export const GuildReminders = () => {
const { guild } = useParams(); const { guild } = useParams();
const { isSuccess, data } = useQuery(fetchGuildReminders(guild)); const { isSuccess, data: guildReminders } = useQuery(fetchGuildReminders(guild));
return ( return (
<div style={{ margin: "0 12px 12px 12px" }}> <div style={{ margin: "0 12px 12px 12px" }}>
@ -57,7 +56,9 @@ export const GuildReminders = () => {
<div id={"guildReminders"}> <div id={"guildReminders"}>
{isSuccess && {isSuccess &&
data.map((reminder) => <EditReminder reminder={reminder}></EditReminder>)} guildReminders.map((reminder) => (
<EditReminder reminder={reminder}></EditReminder>
))}
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,18 @@
import { useQuery } from "react-query";
import { fetchGuildInfo } from "../../api";
import { useParams } from "wouter";
import { GuildReminders } from "./GuildReminders";
import { GuildError } from "./GuildError";
export const Guild = () => {
const { guild } = useParams();
const { isSuccess, data: guildInfo } = useQuery(fetchGuildInfo(guild));
if (!isSuccess) {
return <></>;
} else if (guildInfo.error) {
return <GuildError />;
} else {
return <GuildReminders />;
}
};

View File

@ -5,6 +5,7 @@ import { postGuildReminder } from "../../../api";
import { useParams } from "wouter"; import { useParams } from "wouter";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { ICON_FLASH_TIME } from "../../../consts"; import { ICON_FLASH_TIME } from "../../../consts";
import { useFlash } from "../../App/FlashContext";
export const CreateButtonRow = () => { export const CreateButtonRow = () => {
const { guild } = useParams(); const { guild } = useParams();
@ -12,17 +13,22 @@ export const CreateButtonRow = () => {
const [recentlyCreated, setRecentlyCreated] = useState(false); const [recentlyCreated, setRecentlyCreated] = useState(false);
const flash = useFlash();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
...postGuildReminder(guild), ...postGuildReminder(guild),
onSuccess: () => { onSuccess: (data) => {
queryClient.invalidateQueries({ if (data.error) {
queryKey: ["GUILD_REMINDERS", guild], flash(data.error);
}); } else {
setRecentlyCreated(true); queryClient.invalidateQueries({
setTimeout(() => { queryKey: ["GUILD_REMINDERS", guild],
setRecentlyCreated(false); });
}, ICON_FLASH_TIME); setRecentlyCreated(true);
setTimeout(() => {
setRecentlyCreated(false);
}, ICON_FLASH_TIME);
}
}, },
}); });

View File

@ -5,14 +5,17 @@ import { useParams } from "wouter";
import { ICON_FLASH_TIME } from "../../../consts"; import { ICON_FLASH_TIME } from "../../../consts";
import { DeleteButton } from "./DeleteButton"; import { DeleteButton } from "./DeleteButton";
import { useReminder } from "../ReminderContext"; import { useReminder } from "../ReminderContext";
import { useFlash } from "../../App/FlashContext";
export const EditButtonRow = () => { export const EditButtonRow = () => {
const { guild } = useParams(); const { guild } = useParams();
const [reminder, setReminder] = useReminder(); const [reminder] = useReminder();
const [recentlySaved, setRecentlySaved] = useState(false); const [recentlySaved, setRecentlySaved] = useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const flash = useFlash();
const mutation = useMutation({ const mutation = useMutation({
...patchGuildReminder(guild), ...patchGuildReminder(guild),
onSuccess: () => { onSuccess: () => {
@ -24,6 +27,9 @@ export const EditButtonRow = () => {
setRecentlySaved(false); setRecentlySaved(false);
}, ICON_FLASH_TIME); }, ICON_FLASH_TIME);
}, },
onError: (error) => {
flash(error);
},
}); });
return ( return (

View File

@ -27,7 +27,7 @@ export const EditReminder = ({ reminder: initialReminder }: Props) => {
<Message /> <Message />
<Settings /> <Settings />
</div> </div>
<EditButtonRow></EditButtonRow> <EditButtonRow />
</div> </div>
</ReminderContext.Provider> </ReminderContext.Provider>
); );

View File

@ -36,9 +36,9 @@ export const IntervalSelector = ({
useEffect(() => { useEffect(() => {
if (seconds || minutes || hours || days || months) { if (seconds || minutes || hours || days || months) {
setInterval({ setInterval({
seconds: seconds + minutes * 60 + hours * 3600, seconds: (seconds || 0) + (minutes || 0) * 60 + (hours || 0) * 3600,
days: days, days: days || 0,
months: months, months: months || 0,
}); });
} else { } else {
clearInterval(); clearInterval();