Update reminders

This commit is contained in:
jude 2023-11-03 22:40:57 +00:00
parent 5dc7ceb8aa
commit 31b25e3533
10 changed files with 181 additions and 72 deletions

View File

@ -1,5 +1,6 @@
import axios from "axios"; import axios from "axios";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { QueryClient } from "react-query";
type UserInfo = { type UserInfo = {
name: string; name: string;
@ -77,15 +78,39 @@ type Template = {
embed_fields: EmbedField[] | null; embed_fields: EmbedField[] | null;
}; };
const USER_INFO_STALE_TIME = 120_000;
const GUILD_INFO_STALE_TIME = 300_000;
const OTHER_STALE_TIME = 15_000;
export const fetchUserInfo = () => ({ export const fetchUserInfo = () => ({
queryKey: ["USER_INFO"], queryKey: ["USER_INFO"],
queryFn: () => axios.get("/dashboard/api/user").then((resp) => resp.data) as Promise<UserInfo>, queryFn: () => axios.get("/dashboard/api/user").then((resp) => resp.data) as Promise<UserInfo>,
staleTime: USER_INFO_STALE_TIME,
}); });
export const fetchUserGuilds = () => ({ export const fetchUserGuilds = () => ({
queryKey: ["USER_GUILDS"], queryKey: ["USER_GUILDS"],
queryFn: () => queryFn: () =>
axios.get("/dashboard/api/user/guilds").then((resp) => resp.data) as Promise<GuildInfo[]>, axios.get("/dashboard/api/user/guilds").then((resp) => resp.data) as Promise<GuildInfo[]>,
staleTime: USER_INFO_STALE_TIME,
});
export const fetchGuildChannels = (guild: string) => ({
queryKey: ["GUILD_CHANNELS", guild],
queryFn: () =>
axios.get(`/dashboard/api/guild/${guild}/channels`).then((resp) => resp.data) as Promise<
ChannelInfo[]
>,
staleTime: GUILD_INFO_STALE_TIME,
});
export const fetchGuildRoles = (guild: string) => ({
queryKey: ["GUILD_ROLES", guild],
queryFn: () =>
axios.get(`/dashboard/api/guild/${guild}/roles`).then((resp) => resp.data) as Promise<
RoleInfo[]
>,
staleTime: GUILD_INFO_STALE_TIME,
}); });
export const fetchGuildReminders = (guild: string) => ({ export const fetchGuildReminders = (guild: string) => ({
@ -101,30 +126,22 @@ export const fetchGuildReminders = (guild: string) => ({
expires: reminder.expires === null ? null : DateTime.fromISO(reminder.expires), expires: reminder.expires === null ? null : DateTime.fromISO(reminder.expires),
})), })),
) as Promise<Reminder[]>, ) as Promise<Reminder[]>,
staleTime: OTHER_STALE_TIME,
}); });
export const fetchGuildChannels = (guild: string) => ({ export const mutateGuildReminder = (guild: string) => ({
queryKey: ["GUILD_CHANNELS", guild], mutationFn: (reminder: Reminder) =>
queryFn: () => axios.patch(`/dashboard/api/guild/${guild}/reminders`, {
axios.get(`/dashboard/api/guild/${guild}/channels`).then((resp) => resp.data) as Promise< ...reminder,
ChannelInfo[] utc_time: reminder.utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"),
>, }),
staleTime: 300,
}); });
export const fetchGuildRoles = (guild: string) => ({ export const fetchGuildTemplates = (guild: string) => ({
queryKey: ["GUILD_ROLES", guild],
queryFn: () =>
axios.get(`/dashboard/api/guild/${guild}/roles`).then((resp) => resp.data) as Promise<
RoleInfo[]
>,
staleTime: 300,
});
export const guildTemplatesQuery = (guild: string) => ({
queryKey: ["GUILD_TEMPLATES", guild], queryKey: ["GUILD_TEMPLATES", guild],
queryFn: () => queryFn: () =>
axios.get(`/dashboard/api/guild/${guild}/channels`).then((resp) => resp.data) as Promise< axios.get(`/dashboard/api/guild/${guild}/templates`).then((resp) => resp.data) as Promise<
Template[] Template[]
>, >,
staleTime: OTHER_STALE_TIME,
}); });

View File

@ -3,7 +3,7 @@ export const Content = ({ value, onChange }) => (
<label class="is-sr-only">Content</label> <label class="is-sr-only">Content</label>
<textarea <textarea
class="message-input autoresize discord-content" class="message-input autoresize discord-content"
placeholder="Content Content..." placeholder="Content..."
maxlength={2000} maxlength={2000}
name="content" name="content"
rows={1} rows={1}

View File

@ -1,6 +1,5 @@
import { fetchGuildChannels, fetchUserInfo, Reminder } from "../../api"; import { fetchGuildChannels, fetchUserInfo, mutateGuildReminder, Reminder } from "../../api";
import { useQueries } from "react-query"; import { useMutation, useQueries, useQueryClient } from "react-query";
import { QueryKeys } from "../../consts";
import { useParams } from "wouter"; import { useParams } from "wouter";
import { Name } from "./Name"; import { Name } from "./Name";
import { Username } from "./Username"; import { Username } from "./Username";
@ -10,6 +9,7 @@ import { useState } from "preact/hooks";
import { IntervalSelector } from "./IntervalSelector"; import { IntervalSelector } from "./IntervalSelector";
import { Embed } from "./Embed"; import { Embed } from "./Embed";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { ICON_FLASH_TIME } from "../../consts";
type Props = { type Props = {
reminder: Reminder; reminder: Reminder;
@ -19,12 +19,27 @@ export const EditReminder = ({ reminder: initialReminder }: Props) => {
const { guild } = useParams(); const { guild } = useParams();
const [reminder, setReminder] = useState(initialReminder); const [reminder, setReminder] = useState(initialReminder);
const queryClient = useQueryClient();
const mutation = useMutation({
...mutateGuildReminder(guild),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["GUILD_REMINDERS", guild],
});
setRecentlySaved(true);
setTimeout(() => {
setRecentlySaved(false);
}, ICON_FLASH_TIME);
},
});
const [ const [
{ isSuccess: channelsFetched, data: guildChannels }, { isSuccess: channelsFetched, data: guildChannels },
{ isSuccess: userFetched, data: userInfo }, { isSuccess: userFetched, data: userInfo },
] = useQueries([fetchGuildChannels(guild), fetchUserInfo()]); ] = useQueries([fetchGuildChannels(guild), fetchUserInfo()]);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [recentlySaved, setRecentlySaved] = useState(false);
if (!channelsFetched || !userFetched) { if (!channelsFetched || !userFetched) {
// todo // todo
@ -202,11 +217,27 @@ export const EditReminder = ({ reminder: initialReminder }: Props) => {
</div> </div>
</div> </div>
<div class="button-row-edit"> <div class="button-row-edit">
<button class="button is-success save-btn"> <button
class="button is-success save-btn"
onClick={() => {
mutation.mutate(reminder);
}}
disabled={mutation.isLoading}
>
<span>Save</span>{" "} <span>Save</span>{" "}
<span class="icon"> {mutation.isLoading ? (
<i class="fas fa-save"></i> <span class="icon">
</span> <i class="fas fa-spin fa-cog"></i>
</span>
) : recentlySaved ? (
<span class="icon">
<i class="fas fa-check"></i>
</span>
) : (
<span class="icon">
<i class="fas fa-save"></i>
</span>
)}
</button> </button>
<button class="button is-warning">{reminder.enabled ? "Disable" : "Enable"}</button> <button class="button is-warning">{reminder.enabled ? "Disable" : "Enable"}</button>
<button class="button is-danger delete-reminder">Delete</button> <button class="button is-danger delete-reminder">Delete</button>

View File

@ -1,6 +1,13 @@
import { ImagePicker } from "../ImagePicker"; import { ImagePicker } from "../ImagePicker";
import { Reminder } from "../../../api";
export const Author = ({ name, icon }) => { type Props = {
name: string;
icon: string;
setReminder: (r: (reminder: Reminder) => void) => void;
};
export const Author = ({ name, icon, setReminder }: Props) => {
return ( return (
<div class="embed-author-box"> <div class="embed-author-box">
<div class="a"> <div class="a">
@ -9,7 +16,12 @@ export const Author = ({ name, icon }) => {
class="is-rounded embed_author_url" class="is-rounded embed_author_url"
url={icon} url={icon}
alt="Image for embed author" alt="Image for embed author"
setImage={() => {}} setImage={(url) => {
setReminder((reminder) => ({
...reminder,
embed_author_url: url,
}));
}}
></ImagePicker> ></ImagePicker>
</p> </p>
</div> </div>
@ -25,6 +37,12 @@ export const Author = ({ name, icon }) => {
maxlength={256} maxlength={256}
name="embed_author" name="embed_author"
value={name} value={name}
onChange={(ev) => {
setReminder((reminder) => ({
...reminder,
embed_author: ev.currentTarget.value,
}));
}}
></textarea> ></textarea>
</div> </div>
</div> </div>

View File

@ -1,10 +1,19 @@
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { HexColorPicker } from "react-colorful"; import { HexColorPicker } from "react-colorful";
import { Modal } from "../../Modal"; import { Modal } from "../../Modal";
import { Reminder } from "../../../api";
export const Color = ({ color: colorProp, onChange }) => { type Props = {
color: string;
setReminder: (r: (reminder: Reminder) => void) => void;
};
function colorToInt(hex: string) {
return parseInt(hex.substring(1), 16);
}
export const Color = ({ color, setReminder }: Props) => {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [color, setColor] = useState(colorProp);
return ( return (
<> <>
@ -12,7 +21,7 @@ export const Color = ({ color: colorProp, onChange }) => {
<ColorModal <ColorModal
color={color} color={color}
setModalOpen={setModalOpen} setModalOpen={setModalOpen}
onSave={setColor} setReminder={setReminder}
></ColorModal> ></ColorModal>
)} )}
<button <button
@ -28,13 +37,19 @@ export const Color = ({ color: colorProp, onChange }) => {
); );
}; };
const ColorModal = ({ setModalOpen, color: colorProp, onSave }) => { const ColorModal = ({ setModalOpen, color, setReminder }) => {
const [color, setColor] = useState(colorProp);
return ( return (
<Modal setModalOpen={setModalOpen} title={"Select Color"} onSubmit={onSave}> <Modal setModalOpen={setModalOpen} title={"Select Color"}>
<div class="colorpicker-container"> <div class="colorpicker-container">
<HexColorPicker color={color} onChange={setColor}></HexColorPicker> <HexColorPicker
color={color}
onChange={(color) => {
setReminder((reminder) => ({
...reminder,
embed_color: colorToInt(color),
}));
}}
></HexColorPicker>
</div> </div>
<br></br> <br></br>
<input <input
@ -42,7 +57,10 @@ const ColorModal = ({ setModalOpen, color: colorProp, onSave }) => {
id="colorInput" id="colorInput"
value={color} value={color}
onChange={(ev) => { onChange={(ev) => {
setColor(ev.currentTarget.value); setReminder((reminder) => ({
...reminder,
embed_color: colorToInt(ev.currentTarget.value),
}));
}} }}
></input> ></input>
</Modal> </Modal>

View File

@ -1,13 +1,26 @@
export const Footer = ({ footer, icon }) => ( import { Reminder } from "../../../api";
import { ImagePicker } from "../ImagePicker";
type Props = {
footer: string;
icon: string;
setReminder: (r: (reminder: Reminder) => void) => void;
};
export const Footer = ({ footer, icon, setReminder }: Props) => (
<div class="embed-footer-box"> <div class="embed-footer-box">
<p class="image is-20x20 customizable"> <p class="image is-20x20 customizable">
<a> <ImagePicker
<img class="is-rounded embed_footer_url"
class="is-rounded embed_footer_url" url={icon}
src={icon || "/static/img/bg.webp"} alt="Footer profile-like image"
alt="Footer profile-like image" setImage={(url: string) => {
></img> setReminder((reminder) => ({
</a> ...reminder,
embed_footer_url: url,
}));
}}
></ImagePicker>
</p> </p>
<label class="is-sr-only" for="embedFooter"> <label class="is-sr-only" for="embedFooter">
Embed Footer text Embed Footer text
@ -19,6 +32,12 @@ export const Footer = ({ footer, icon }) => (
name="embed_footer" name="embed_footer"
rows={1} rows={1}
value={footer} value={footer}
onChange={(ev) => {
setReminder((reminder) => ({
...reminder,
embed_footer: ev.currentTarget.value,
}));
}}
></textarea> ></textarea>
</div> </div>
); );

View File

@ -6,25 +6,31 @@ import { Color } from "./Color";
import { Reminder } from "../../../api"; import { Reminder } from "../../../api";
import { ImagePicker } from "../ImagePicker"; import { ImagePicker } from "../ImagePicker";
function colorToInt(hex: string) { function intToColor(num: number) {
return parseInt(hex.substring(1), 16); return `#${num.toString(16).padStart(6, "0")}`;
} }
const DEFAULT_COLOR = 9418359;
export const Embed = ({ reminder, setReminder }) => { export const Embed = ({ reminder, setReminder }) => {
return ( return (
<div class="discord-embed"> <div
class="discord-embed"
style={{
borderLeftColor: intToColor(reminder.embed_color || DEFAULT_COLOR),
}}
>
<div class="embed-body"> <div class="embed-body">
<Color <Color
color={reminder.embed_color} color={intToColor(reminder.embed_color || DEFAULT_COLOR)}
onChange={(color: string) => { setReminder={setReminder}
setReminder((reminder: Reminder) => ({
...reminder,
embed_color: colorToInt(color),
}));
}}
></Color> ></Color>
<div class="a"> <div class="a">
<Author name={reminder.embed_author} icon={reminder.embed_author_url}></Author> <Author
name={reminder.embed_author}
icon={reminder.embed_author_url}
setReminder={setReminder}
></Author>
<Title <Title
title={reminder.embed_title} title={reminder.embed_title}
onChange={(title: string) => onChange={(title: string) =>
@ -83,6 +89,7 @@ export const Embed = ({ reminder, setReminder }) => {
<p class="image thumbnail customizable"> <p class="image thumbnail customizable">
<ImagePicker <ImagePicker
class="embed_thumbnail_url" class="embed_thumbnail_url"
url={reminder.embed_thumbnail_url}
alt="Square thumbnail embedded image" alt="Square thumbnail embedded image"
setImage={() => {}} setImage={() => {}}
></ImagePicker> ></ImagePicker>
@ -93,12 +100,17 @@ export const Embed = ({ reminder, setReminder }) => {
<p class="image is-400x300 customizable"> <p class="image is-400x300 customizable">
<ImagePicker <ImagePicker
class="embed_image_url" class="embed_image_url"
url={reminder.embed_image_url}
alt="Large embedded image" alt="Large embedded image"
setImage={() => {}} setImage={() => {}}
></ImagePicker> ></ImagePicker>
</p> </p>
<Footer footer={reminder.embed_footer} icon={reminder.embed_footer_url}></Footer> <Footer
footer={reminder.embed_footer}
icon={reminder.embed_footer_url}
setReminder={setReminder}
></Footer>
</div> </div>
); );
}; };

View File

@ -1,7 +1,7 @@
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Modal } from "../Modal"; import { Modal } from "../Modal";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { guildTemplatesQuery } from "../../api"; import { fetchGuildTemplates } from "../../api";
import { useParams } from "wouter"; import { useParams } from "wouter";
export const LoadTemplate = ({ setReminder }) => { export const LoadTemplate = ({ setReminder }) => {
@ -24,16 +24,17 @@ export const LoadTemplate = ({ setReminder }) => {
const LoadTemplateModal = ({ setModalOpen }) => { const LoadTemplateModal = ({ setModalOpen }) => {
const { guild } = useParams(); const { guild } = useParams();
const { status, data: templates } = useQuery(guildTemplatesQuery(guild)); const { status, data: templates } = useQuery(fetchGuildTemplates(guild));
return ( return (
<Modal setModalOpen={setModalOpen} title={"Load Template"}> <Modal setModalOpen={setModalOpen} title={"Load Template"}>
<div class="control has-icons-left"> <div class="control has-icons-left">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select id="templateSelect"> <select id="templateSelect">
{templates.map((template) => ( {templates !== undefined &&
<option value={template.id}>{template.name}</option> templates.map((template) => (
))} <option value={template.id}>{template.name}</option>
))}
</select> </select>
</div> </div>
<div class="icon is-small is-left"> <div class="icon is-small is-left">

View File

@ -5,7 +5,6 @@ import { Brand } from "./Brand";
import { Wave } from "./Wave"; import { Wave } from "./Wave";
import { GuildEntry } from "./GuildEntry"; import { GuildEntry } from "./GuildEntry";
import { fetchUserGuilds, GuildInfo } from "../../api"; import { fetchUserGuilds, GuildInfo } from "../../api";
import { QueryKeys } from "../../consts";
type ContentProps = { type ContentProps = {
guilds: GuildInfo[]; guilds: GuildInfo[];
@ -60,11 +59,7 @@ const SidebarContent = ({ guilds }: ContentProps) => {
}; };
export const Sidebar = () => { export const Sidebar = () => {
const { status, data } = useQuery({ const { status, data } = useQuery(fetchUserGuilds());
queryKey: [QueryKeys.USER_GUILDS],
queryFn: fetchUserGuilds,
staleTime: Infinity,
});
let content = <SidebarContent guilds={[]}></SidebarContent>; let content = <SidebarContent guilds={[]}></SidebarContent>;
if (status === "success") { if (status === "success") {

View File

@ -1,4 +1,2 @@
export enum QueryKeys { export const ICON_FLASH_TIME = 2_500;
USER_GUILDS = "userGuilds", export const MESSAGE_FLASH_TIME = 5_000;
GUILD_REMINDERS = "guildReminders",
}