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 { DateTime } from "luxon";
import { QueryClient } from "react-query";
type UserInfo = {
name: string;
@ -77,15 +78,39 @@ type Template = {
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 = () => ({
queryKey: ["USER_INFO"],
queryFn: () => axios.get("/dashboard/api/user").then((resp) => resp.data) as Promise<UserInfo>,
staleTime: USER_INFO_STALE_TIME,
});
export const fetchUserGuilds = () => ({
queryKey: ["USER_GUILDS"],
queryFn: () =>
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) => ({
@ -101,30 +126,22 @@ export const fetchGuildReminders = (guild: string) => ({
expires: reminder.expires === null ? null : DateTime.fromISO(reminder.expires),
})),
) as Promise<Reminder[]>,
staleTime: OTHER_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: 300,
export const mutateGuildReminder = (guild: string) => ({
mutationFn: (reminder: Reminder) =>
axios.patch(`/dashboard/api/guild/${guild}/reminders`, {
...reminder,
utc_time: reminder.utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"),
}),
});
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: 300,
});
export const guildTemplatesQuery = (guild: string) => ({
export const fetchGuildTemplates = (guild: string) => ({
queryKey: ["GUILD_TEMPLATES", guild],
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[]
>,
staleTime: OTHER_STALE_TIME,
});

View File

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

View File

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

View File

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

View File

@ -1,10 +1,19 @@
import { useState } from "preact/hooks";
import { HexColorPicker } from "react-colorful";
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 [color, setColor] = useState(colorProp);
return (
<>
@ -12,7 +21,7 @@ export const Color = ({ color: colorProp, onChange }) => {
<ColorModal
color={color}
setModalOpen={setModalOpen}
onSave={setColor}
setReminder={setReminder}
></ColorModal>
)}
<button
@ -28,13 +37,19 @@ export const Color = ({ color: colorProp, onChange }) => {
);
};
const ColorModal = ({ setModalOpen, color: colorProp, onSave }) => {
const [color, setColor] = useState(colorProp);
const ColorModal = ({ setModalOpen, color, setReminder }) => {
return (
<Modal setModalOpen={setModalOpen} title={"Select Color"} onSubmit={onSave}>
<Modal setModalOpen={setModalOpen} title={"Select Color"}>
<div class="colorpicker-container">
<HexColorPicker color={color} onChange={setColor}></HexColorPicker>
<HexColorPicker
color={color}
onChange={(color) => {
setReminder((reminder) => ({
...reminder,
embed_color: colorToInt(color),
}));
}}
></HexColorPicker>
</div>
<br></br>
<input
@ -42,7 +57,10 @@ const ColorModal = ({ setModalOpen, color: colorProp, onSave }) => {
id="colorInput"
value={color}
onChange={(ev) => {
setColor(ev.currentTarget.value);
setReminder((reminder) => ({
...reminder,
embed_color: colorToInt(ev.currentTarget.value),
}));
}}
></input>
</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">
<p class="image is-20x20 customizable">
<a>
<img
class="is-rounded embed_footer_url"
src={icon || "/static/img/bg.webp"}
alt="Footer profile-like image"
></img>
</a>
<ImagePicker
class="is-rounded embed_footer_url"
url={icon}
alt="Footer profile-like image"
setImage={(url: string) => {
setReminder((reminder) => ({
...reminder,
embed_footer_url: url,
}));
}}
></ImagePicker>
</p>
<label class="is-sr-only" for="embedFooter">
Embed Footer text
@ -19,6 +32,12 @@ export const Footer = ({ footer, icon }) => (
name="embed_footer"
rows={1}
value={footer}
onChange={(ev) => {
setReminder((reminder) => ({
...reminder,
embed_footer: ev.currentTarget.value,
}));
}}
></textarea>
</div>
);

View File

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

View File

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

View File

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

View File

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