From 136468b4bd12e085027fbbb1ba01f44399e33756 Mon Sep 17 00:00:00 2001 From: aronmal Date: Mon, 18 Mar 2024 16:24:00 +0100 Subject: [PATCH] feat: Finished saving functionality for dashboard --- add-test-server.http | 11 + src/lib/responseBuilders.ts | 10 + src/routes/config/[guildId].tsx | 543 +++++++++++++++++++++++--------- src/routes/config/index.tsx | 127 ++++---- src/styles/pages/config.scss | 41 +-- 5 files changed, 499 insertions(+), 233 deletions(-) create mode 100644 add-test-server.http diff --git a/add-test-server.http b/add-test-server.http new file mode 100644 index 0000000..c4b673a --- /dev/null +++ b/add-test-server.http @@ -0,0 +1,11 @@ +POST http://localhost:3000/api/598539452343648256/config +# Content-Type: application/json +Authorization: Basic {{$dotenv DISCORD_CLIENT_ID}}:{{$dotenv DISCORD_CLIENT_SECRET}} +Origin: http://localhost:3000 + +### + +DELETE http://localhost:3000/api/598539452343648256/config +# Content-Type: application/json +Authorization: Basic {{$dotenv DISCORD_CLIENT_ID}}:{{$dotenv DISCORD_CLIENT_SECRET}} +Origin: http://localhost:3000 \ No newline at end of file diff --git a/src/lib/responseBuilders.ts b/src/lib/responseBuilders.ts index d8c4c1c..3bebb91 100644 --- a/src/lib/responseBuilders.ts +++ b/src/lib/responseBuilders.ts @@ -35,6 +35,16 @@ export const splitInterval = (tpInterval: number) => { return { targetMinute, targetHour, targetDay }; }; +export const combineInterval = ( + targetMinute: number, + targetHour: number, + targetDay: number, +) => { + const tpInterval = targetMinute | (targetHour << 6) | (targetDay << 11); + + return tpInterval; +}; + export const DayKeys = ["0", "1", "2", "3", "4", "5", "6"] as const; export type DayKeys = (typeof DayKeys)[number]; export type Messages = Record; diff --git a/src/routes/config/[guildId].tsx b/src/routes/config/[guildId].tsx index cc91422..db0e5e5 100644 --- a/src/routes/config/[guildId].tsx +++ b/src/routes/config/[guildId].tsx @@ -1,11 +1,16 @@ import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons"; import { useLocation, useNavigate, useParams } from "@solidjs/router"; +import { eq } from "drizzle-orm"; +import { PgUpdateSetSource } from "drizzle-orm/pg-core"; import moment from "moment-timezone"; import createClient from "openapi-fetch"; import { For, Index, + Show, + batch, createEffect, + createMemo, createResource, createSignal, } from "solid-js"; @@ -13,7 +18,11 @@ import { createStore } from "solid-js/store"; import { getRequestEvent } from "solid-js/web"; import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"; import Layout from "~/components/Layout"; +import db from "~/drizzle"; +import { guilds } from "~/drizzle/schema"; import getAccessToken from "~/lib/accessToken"; +import { combineInterval, splitInterval } from "~/lib/responseBuilders"; +import { zodBigIntId } from "~/lib/zod"; import { paths } from "~/types/discord"; import "../../styles/pages/config.scss"; @@ -22,26 +31,37 @@ if (typeof import.meta.env.VITE_DISCORD_BOT_TOKEN === "undefined") const guessTZ = () => Intl.DateTimeFormat().resolvedOptions().timeZone; -const initialValue = (params: ReturnType) => ({ - success: null as boolean | null, - guild: { - id: params.guildId, - name: undefined as string | undefined, - icon: undefined as string | null | undefined, - channel: "", - channels: [] as { id: string; name: string }[], - }, - tzNames: [guessTZ()], -}); - const getPayload = async ( id: string, ): Promise< | { success: false; message: string } - | (ReturnType & { success: true }) + | { + success: true; + guild: { + id: string; + name: string; + icon: string | null | undefined; + tpChannelId: string; + channels: { + id: string; + name: string; + }[]; + tpInterval: number; + pingableRoles: boolean; + timezone: string; + }; + tzNames: string[]; + } > => { "use server"; + let guildId: bigint; + try { + guildId = zodBigIntId.parse(id); + } catch (e) { + return { success: false, message: "ID is invalid" }; + } + const event = getRequestEvent(); if (!event) return { success: false, message: "No request event!" }; @@ -63,7 +83,7 @@ const getPayload = async ( { params: { path: { - guild_id: id, + guild_id: String(guildId), }, }, headers: { @@ -81,7 +101,7 @@ const getPayload = async ( }; } - const guild = guildsData?.find((e) => e.id === id); + const guild = guildsData?.find((e) => e.id === String(guildId)); if (!guild) return { @@ -95,7 +115,10 @@ const getPayload = async ( "User is no MANAGE_GUILD permissions on this guild with requested id!", }; - const channels: ReturnType["guild"]["channels"] = []; + const channels: { + id: string; + name: string; + }[] = []; channelsData?.forEach((channel) => { if (channel.type !== 0) return; channels.push({ @@ -104,6 +127,12 @@ const getPayload = async ( }); }); + const config = await db.query.guilds + .findFirst({ where: eq(guilds.id, guildId) }) + .execute(); + + if (!config) return { success: false, message: "No config found!" }; + console.log(new URL(event.request.url).pathname, "success"); return { @@ -112,47 +141,80 @@ const getPayload = async ( id: guild.id, name: guild.name, icon: guild.icon, - channel: "", + tpChannelId: String(config.tpChannelId ?? ""), channels, + tpInterval: config.tpInterval, + pingableRoles: config.tpRolesEnabled, + timezone: config.timezone, }, tzNames: moment.tz.names(), }; }; +const saveConfig = async ( + id: string, + updateValues: PgUpdateSetSource, +): Promise<{ success: false; message: string } | { success: true }> => { + "use server"; + + let guildId: bigint; + try { + guildId = zodBigIntId.parse(id); + } catch (e) { + return { success: false, message: "ID is invalid" }; + } + + const event = getRequestEvent(); + if (!event) return { success: false, message: "No request event!" }; + + console.log({ updateValues }); + + await db + .update(guilds) + .set(updateValues) + .where(eq(guilds.id, guildId)) + .execute(); + + console.log(new URL(event.request.url).pathname, "config save success"); + + return { success: true }; +}; + function config() { const params = useParams(); const navigator = useNavigate(); const location = useLocation(); const [timezoneRef, setTimezoneRef] = createSignal(); - const [timePlanningRef, setTimePlanningRef] = - createSignal(); + const [tpEnabledRef, setTpEnabledRef] = createSignal(); const [channelRef, setChannelRef] = createSignal(); + const [targetMinuteRef, setTargetMinuteRef] = + createSignal(); + const [targetHourRef, setTargetHourRef] = createSignal(); + const [targetDayRef, setTargetDayRef] = createSignal(); const [pingableRolesRef, setPingableRolesRef] = createSignal(); - const [timezone, setTimezone] = createSignal(guessTZ()); - const [payload] = createResource( + const [payload, { refetch }] = createResource( params.guildId, async (id) => { - const payload = await getPayload(id).catch((e) => console.warn(e, id)); + const payload = await getPayload(id); if (!payload) { console.error(location.pathname, payload); - return initialValue(params); + return undefined; } if (!payload.success) { console.log(payload); console.log(location.pathname, payload.message, "No success"); navigator("/config", { replace: false }); - return initialValue(params); + return undefined; } return payload; }, { - initialValue: initialValue(params), deferStream: true, }, ); @@ -160,168 +222,337 @@ function config() { features: { timePlanning: { enabled: false, - channelId: "833442323160891452", + channelId: "", + targetMinute: 0, + targetHour: 0, + targetDay: 0, pingableRoles: false, + timezone: guessTZ(), }, }, }); + const updateValues = createMemo(() => { + const guild = payload()?.guild; + if (!guild) return {}; + const data = config.features.timePlanning; + const tpInterval = combineInterval( + data.targetMinute, + data.targetHour, + data.targetDay, + ); + + const result: PgUpdateSetSource = { + timezone: data.timezone !== guild.timezone ? data.timezone : undefined, + tpChannelId: + data.enabled || !data.channelId + ? data.channelId && data.channelId !== guild.tpChannelId + ? BigInt(data.channelId) + : undefined + : null, + tpInterval: + data.enabled && data.channelId && tpInterval !== guild.tpInterval + ? tpInterval + : undefined, + tpRolesEnabled: + data.enabled && + data.channelId && + data.pingableRoles !== guild.pingableRoles + ? data.pingableRoles + : undefined, + }; + return result; + }); + const willUpdateValues = () => + Object.values(updateValues()).filter((e) => typeof e !== "undefined") + .length; + createEffect(() => { - const channelId = payload().guild.channel; - setConfig("features", "timePlanning", "channelId", channelId); - const ref = channelRef(); - if (!ref) return; - ref.value = channelId; + const guild = payload()?.guild; + if (!guild) return; + const channelId = guild.tpChannelId; + const pingableRoles = guild.pingableRoles; + const { targetMinute, targetHour, targetDay } = splitInterval( + guild.tpInterval, + ); + const timezone = guild.timezone; + + batch(() => { + setConfig("features", "timePlanning", "enabled", !!channelId); + setConfig("features", "timePlanning", "channelId", channelId); + setConfig("features", "timePlanning", "targetMinute", targetMinute); + setConfig("features", "timePlanning", "targetHour", targetHour); + setConfig("features", "timePlanning", "targetDay", targetDay); + setConfig("features", "timePlanning", "pingableRoles", pingableRoles); + setConfig("features", "timePlanning", "timezone", timezone); + }); }); createEffect(() => { const ref = timezoneRef(); - if (!ref) return; - ref.value = timezone(); + if (ref) ref.value = config.features.timePlanning.timezone; }); createEffect(() => { - const ref = timePlanningRef(); - if (!ref) return; - ref.checked = config.features.timePlanning.enabled; + const ref = tpEnabledRef(); + if (ref) ref.checked = config.features.timePlanning.enabled; + }); + createEffect(() => { + const ref = channelRef(); + if (ref) ref.value = config.features.timePlanning.channelId; + }); + createEffect(() => { + const ref = targetMinuteRef(); + if (ref) ref.value = String(config.features.timePlanning.targetMinute); + }); + createEffect(() => { + const ref = targetHourRef(); + if (ref) ref.value = String(config.features.timePlanning.targetHour); + }); + createEffect(() => { + const ref = targetDayRef(); + if (ref) ref.value = String(config.features.timePlanning.targetDay); }); createEffect(() => { const ref = pingableRolesRef(); - if (!ref) return; - ref.checked = config.features.timePlanning.pingableRoles; + if (ref) ref.checked = config.features.timePlanning.pingableRoles; }); return ( -

Configure li'l Judd in

-
+
+

Configure li'l Judd in

-
- Server pfp -

{payload().guild.name}

+
+
+ Server pfp +

{payload()?.guild.name}

+
-
-
-

Guild

-

General settings for this guild.

-
- - setTimezoneRef(e)} - // disabled={!tzNames().find((e) => e === timezone())} - onInput={(e) => setTimezone(e.target.value)} - /> - - - - {(zone) => - - - -
-
- -
-

Features

-

Configure the features of the bot

- - setTimePlanningRef(e)} - onInput={(e) => - setConfig("features", "timePlanning", "enabled", e.target.checked) - } - /> -
+
+

Guild

+

General settings for this guild.

- - setTimezoneRef(e)} onInput={(e) => setConfig( "features", "timePlanning", - "channelId", + "timezone", e.target.value, ) } - > - - - {(channel) => ( - - )} - - -
-
- - setPingableRolesRef(e)} - onInput={(e) => - setConfig( - "features", - "timePlanning", - "pingableRoles", - e.target.checked, - ) - } /> -
-
-
-
- - -
+ + + {(zone) => + + + +
+ + +
+

Features

+

Configure the features of the bot

+ + setTpEnabledRef(e)} + onInput={(e) => + setConfig( + "features", + "timePlanning", + "enabled", + e.target.checked, + ) + } + /> +
+
+ + + + {"<-- or changes won't be saved"} + +
+
+ + + at + + : + +
+
+ + setPingableRolesRef(e)} + onInput={(e) => + setConfig( + "features", + "timePlanning", + "pingableRoles", + e.target.checked, + ) + } + /> +
+
+
+ +
+ + + UNSAVED CHANGES +
+
); diff --git a/src/routes/config/index.tsx b/src/routes/config/index.tsx index e5630cc..b2ab27d 100644 --- a/src/routes/config/index.tsx +++ b/src/routes/config/index.tsx @@ -1,10 +1,12 @@ import { faPlusCircle, faWrench } from "@fortawesome/pro-regular-svg-icons"; import { useLocation, useNavigate } from "@solidjs/router"; import createClient from "openapi-fetch"; -import { For, createResource } from "solid-js"; +import { For, Show, createResource } from "solid-js"; import { getRequestEvent } from "solid-js/web"; import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"; import Layout from "~/components/Layout"; +import db from "~/drizzle"; +import { guilds } from "~/drizzle/schema"; import getAccessToken from "~/lib/accessToken"; import { paths } from "~/types/discord"; import "../../styles/pages/config.scss"; @@ -15,13 +17,16 @@ if (typeof import.meta.env.VITE_DISCORD_CLIENT_ID === "undefined") if (typeof import.meta.env.VITE_DISCORD_OAUTH2_PERMISSIONS === "undefined") throw new Error("No env VITE_DISCORD_OAUTH2_PERMISSIONS found!"); +interface Guild { + id: string; + name: string; + icon: string | null | undefined; +} + const initialValue = () => ({ success: null as boolean | null, - guilds: [] as { - id: string; - name: string; - icon: string | null | undefined; - }[], + joined: [] as Guild[], + canJoin: [] as Guild[], }); const getPayload = async (): Promise< @@ -43,9 +48,10 @@ const getPayload = async (): Promise< const { GET } = createClient({ baseUrl: "https://discord.com/api/v10", }); - const { data: guilds, error } = await GET("/users/@me/guilds", { + const { data, error } = await GET("/users/@me/guilds", { headers: { Authorization: `Bearer ${tokens.accessToken}` }, }); + const configs = await db.select().from(guilds).execute(); if (error) { console.log("Discord api error:", { error }); @@ -53,12 +59,21 @@ const getPayload = async (): Promise< } console.log(pathname, "success"); + const joined: Guild[] = []; + const canJoin: Guild[] = []; + + data + ?.filter((e) => parseInt(e.permissions) & (1 << 5)) + .forEach(({ id, name, icon }) => { + configs.map((e) => e.id.toString()).includes(id) + ? joined.push({ id, name, icon }) + : canJoin.push({ id, name, icon }); + }); + return { success: true, - guilds: - guilds - ?.filter((e) => parseInt(e.permissions) & (1 << 5)) - .map(({ id, name, icon }) => ({ id, name, icon })) ?? [], + joined, + canJoin, }; }; @@ -84,64 +99,62 @@ function index() { return payload; }, - { deferStream: true }, + { deferStream: true, initialValue: initialValue() }, ); return (

Configure li'l Judd in

-
- - {(guild) => { - return ( - - Server pfp -

{guild.name}

- -
- ); - }} -
-
+

Add li'l Judd to

-
- - {(guild) => { - return ( - - Server pfp -

{guild.name}

- -
- ); - }} -
-
+
); } +function GuildList(props: { list: Guild[]; joined: boolean }) { + return ( +
+ Nothing here
} + > + + {(guild) => { + return ( + + Server pfp +

{guild.name}

+ +
+ ); + }} +
+ + + ); +} + export default index; diff --git a/src/styles/pages/config.scss b/src/styles/pages/config.scss index a3a5fc5..c13debc 100644 --- a/src/styles/pages/config.scss +++ b/src/styles/pages/config.scss @@ -16,8 +16,7 @@ margin-right: 10px; } - section, - a { + .box { transition: 0.3s; transition-timing-function: ease-out; background-color: rgba(0, 0, 0, 0.5); @@ -28,28 +27,30 @@ margin: 1rem auto; width: 100%; - svg { - padding: 0 1rem; + &.effect { + svg { + padding: 0 1rem; - path { - transition: 0.1s; - transform: translateX(100%); - opacity: 0; + path { + transition: 0.1s; + transform: translateX(100%); + opacity: 0; + } } - } - &:hover { - transition: 1s; - transition-delay: 0.1s; - background-color: rgba(0, 0, 0, 0.4); - border: 2px solid rgba(255, 255, 255, 0.5); - - svg path { - transition: 0.5s; + &:hover { + transition: 1s; transition-delay: 0.1s; - transition-timing-function: ease-out; - transform: translateX(0); - opacity: 1; + background-color: rgba(0, 0, 0, 0.4); + border: 2px solid rgba(255, 255, 255, 0.5); + + svg path { + transition: 0.5s; + transition-delay: 0.1s; + transition-timing-function: ease-out; + transform: translateX(0); + opacity: 1; + } } } }