feat: Finished saving functionality for dashboard
This commit is contained in:
parent
3c404ab5fa
commit
136468b4bd
5 changed files with 499 additions and 233 deletions
11
add-test-server.http
Normal file
11
add-test-server.http
Normal file
|
@ -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
|
|
@ -35,6 +35,16 @@ export const splitInterval = (tpInterval: number) => {
|
||||||
return { targetMinute, targetHour, targetDay };
|
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 const DayKeys = ["0", "1", "2", "3", "4", "5", "6"] as const;
|
||||||
export type DayKeys = (typeof DayKeys)[number];
|
export type DayKeys = (typeof DayKeys)[number];
|
||||||
export type Messages = Record<DayKeys, string | null>;
|
export type Messages = Record<DayKeys, string | null>;
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons";
|
import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons";
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router";
|
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 moment from "moment-timezone";
|
||||||
import createClient from "openapi-fetch";
|
import createClient from "openapi-fetch";
|
||||||
import {
|
import {
|
||||||
For,
|
For,
|
||||||
Index,
|
Index,
|
||||||
|
Show,
|
||||||
|
batch,
|
||||||
createEffect,
|
createEffect,
|
||||||
|
createMemo,
|
||||||
createResource,
|
createResource,
|
||||||
createSignal,
|
createSignal,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
|
@ -13,7 +18,11 @@ import { createStore } from "solid-js/store";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||||
import Layout from "~/components/Layout";
|
import Layout from "~/components/Layout";
|
||||||
|
import db from "~/drizzle";
|
||||||
|
import { guilds } from "~/drizzle/schema";
|
||||||
import getAccessToken from "~/lib/accessToken";
|
import getAccessToken from "~/lib/accessToken";
|
||||||
|
import { combineInterval, splitInterval } from "~/lib/responseBuilders";
|
||||||
|
import { zodBigIntId } from "~/lib/zod";
|
||||||
import { paths } from "~/types/discord";
|
import { paths } from "~/types/discord";
|
||||||
import "../../styles/pages/config.scss";
|
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 guessTZ = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
const initialValue = (params: ReturnType<typeof useParams>) => ({
|
|
||||||
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 (
|
const getPayload = async (
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<
|
): Promise<
|
||||||
| { success: false; message: string }
|
| { success: false; message: string }
|
||||||
| (ReturnType<typeof initialValue> & { 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";
|
"use server";
|
||||||
|
|
||||||
|
let guildId: bigint;
|
||||||
|
try {
|
||||||
|
guildId = zodBigIntId.parse(id);
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, message: "ID is invalid" };
|
||||||
|
}
|
||||||
|
|
||||||
const event = getRequestEvent();
|
const event = getRequestEvent();
|
||||||
if (!event) return { success: false, message: "No request event!" };
|
if (!event) return { success: false, message: "No request event!" };
|
||||||
|
|
||||||
|
@ -63,7 +83,7 @@ const getPayload = async (
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
path: {
|
path: {
|
||||||
guild_id: id,
|
guild_id: String(guildId),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
headers: {
|
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)
|
if (!guild)
|
||||||
return {
|
return {
|
||||||
|
@ -95,7 +115,10 @@ const getPayload = async (
|
||||||
"User is no MANAGE_GUILD permissions on this guild with requested id!",
|
"User is no MANAGE_GUILD permissions on this guild with requested id!",
|
||||||
};
|
};
|
||||||
|
|
||||||
const channels: ReturnType<typeof initialValue>["guild"]["channels"] = [];
|
const channels: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}[] = [];
|
||||||
channelsData?.forEach((channel) => {
|
channelsData?.forEach((channel) => {
|
||||||
if (channel.type !== 0) return;
|
if (channel.type !== 0) return;
|
||||||
channels.push({
|
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");
|
console.log(new URL(event.request.url).pathname, "success");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -112,47 +141,80 @@ const getPayload = async (
|
||||||
id: guild.id,
|
id: guild.id,
|
||||||
name: guild.name,
|
name: guild.name,
|
||||||
icon: guild.icon,
|
icon: guild.icon,
|
||||||
channel: "",
|
tpChannelId: String(config.tpChannelId ?? ""),
|
||||||
channels,
|
channels,
|
||||||
|
tpInterval: config.tpInterval,
|
||||||
|
pingableRoles: config.tpRolesEnabled,
|
||||||
|
timezone: config.timezone,
|
||||||
},
|
},
|
||||||
tzNames: moment.tz.names(),
|
tzNames: moment.tz.names(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveConfig = async (
|
||||||
|
id: string,
|
||||||
|
updateValues: PgUpdateSetSource<typeof guilds>,
|
||||||
|
): 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() {
|
function config() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigator = useNavigate();
|
const navigator = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [timezoneRef, setTimezoneRef] = createSignal<HTMLInputElement>();
|
const [timezoneRef, setTimezoneRef] = createSignal<HTMLInputElement>();
|
||||||
const [timePlanningRef, setTimePlanningRef] =
|
const [tpEnabledRef, setTpEnabledRef] = createSignal<HTMLInputElement>();
|
||||||
createSignal<HTMLInputElement>();
|
|
||||||
const [channelRef, setChannelRef] = createSignal<HTMLSelectElement>();
|
const [channelRef, setChannelRef] = createSignal<HTMLSelectElement>();
|
||||||
|
const [targetMinuteRef, setTargetMinuteRef] =
|
||||||
|
createSignal<HTMLSelectElement>();
|
||||||
|
const [targetHourRef, setTargetHourRef] = createSignal<HTMLSelectElement>();
|
||||||
|
const [targetDayRef, setTargetDayRef] = createSignal<HTMLSelectElement>();
|
||||||
const [pingableRolesRef, setPingableRolesRef] =
|
const [pingableRolesRef, setPingableRolesRef] =
|
||||||
createSignal<HTMLInputElement>();
|
createSignal<HTMLInputElement>();
|
||||||
|
|
||||||
const [timezone, setTimezone] = createSignal(guessTZ());
|
const [payload, { refetch }] = createResource(
|
||||||
const [payload] = createResource(
|
|
||||||
params.guildId,
|
params.guildId,
|
||||||
async (id) => {
|
async (id) => {
|
||||||
const payload = await getPayload(id).catch((e) => console.warn(e, id));
|
const payload = await getPayload(id);
|
||||||
|
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
console.error(location.pathname, payload);
|
console.error(location.pathname, payload);
|
||||||
return initialValue(params);
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!payload.success) {
|
if (!payload.success) {
|
||||||
console.log(payload);
|
console.log(payload);
|
||||||
console.log(location.pathname, payload.message, "No success");
|
console.log(location.pathname, payload.message, "No success");
|
||||||
navigator("/config", { replace: false });
|
navigator("/config", { replace: false });
|
||||||
return initialValue(params);
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
initialValue: initialValue(params),
|
|
||||||
deferStream: true,
|
deferStream: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -160,168 +222,337 @@ function config() {
|
||||||
features: {
|
features: {
|
||||||
timePlanning: {
|
timePlanning: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
channelId: "833442323160891452",
|
channelId: "",
|
||||||
|
targetMinute: 0,
|
||||||
|
targetHour: 0,
|
||||||
|
targetDay: 0,
|
||||||
pingableRoles: false,
|
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<typeof guilds> = {
|
||||||
|
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(() => {
|
createEffect(() => {
|
||||||
const channelId = payload().guild.channel;
|
const guild = payload()?.guild;
|
||||||
setConfig("features", "timePlanning", "channelId", channelId);
|
if (!guild) return;
|
||||||
const ref = channelRef();
|
const channelId = guild.tpChannelId;
|
||||||
if (!ref) return;
|
const pingableRoles = guild.pingableRoles;
|
||||||
ref.value = channelId;
|
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(() => {
|
createEffect(() => {
|
||||||
const ref = timezoneRef();
|
const ref = timezoneRef();
|
||||||
if (!ref) return;
|
if (ref) ref.value = config.features.timePlanning.timezone;
|
||||||
ref.value = timezone();
|
|
||||||
});
|
});
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const ref = timePlanningRef();
|
const ref = tpEnabledRef();
|
||||||
if (!ref) return;
|
if (ref) ref.checked = config.features.timePlanning.enabled;
|
||||||
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(() => {
|
createEffect(() => {
|
||||||
const ref = pingableRolesRef();
|
const ref = pingableRolesRef();
|
||||||
if (!ref) return;
|
if (ref) ref.checked = config.features.timePlanning.pingableRoles;
|
||||||
ref.checked = config.features.timePlanning.pingableRoles;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout site="config">
|
<Layout site="config">
|
||||||
<h3>Configure li'l Judd in</h3>
|
<div class="group">
|
||||||
<div>
|
<h3>Configure li'l Judd in</h3>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex-row centered">
|
<div>
|
||||||
<img
|
<div class="flex-row centered">
|
||||||
class="guildpfp"
|
<img
|
||||||
src={
|
class="guildpfp"
|
||||||
payload().guild.icon
|
src={
|
||||||
? `https://cdn.discordapp.com/icons/${payload().guild.id}/${
|
payload()?.guild.icon
|
||||||
payload().guild.icon
|
? `https://cdn.discordapp.com/icons/${payload()?.guild.id}/${
|
||||||
}.webp?size=240`
|
payload()?.guild.icon
|
||||||
: "https://cdn.discordapp.com/icons/1040502664506646548/bb5a51c4659cf47bdd942bb11e974da7.webp?size=240"
|
}.webp?size=240`
|
||||||
}
|
: "https://cdn.discordapp.com/icons/1040502664506646548/bb5a51c4659cf47bdd942bb11e974da7.webp?size=240"
|
||||||
alt="Server pfp"
|
}
|
||||||
/>
|
alt="Server pfp"
|
||||||
<h1>{payload().guild.name}</h1>
|
/>
|
||||||
|
<h1>{payload()?.guild.name}</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<section>
|
<section class="box">
|
||||||
<h2>Guild</h2>
|
<h2>Guild</h2>
|
||||||
<p>General settings for this guild.</p>
|
<p>General settings for this guild.</p>
|
||||||
<div class="flex-row">
|
|
||||||
<label for="timezone">Timezone for your server:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
list="timezones"
|
|
||||||
id="timezone"
|
|
||||||
ref={(e) => setTimezoneRef(e)}
|
|
||||||
// disabled={!tzNames().find((e) => e === timezone())}
|
|
||||||
onInput={(e) => setTimezone(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<datalist id="timezones">
|
|
||||||
<Index each={payload().tzNames}>
|
|
||||||
{(zone) => <option value={zone()} />}
|
|
||||||
</Index>
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
<button
|
|
||||||
disabled={guessTZ() === timezone()}
|
|
||||||
title={"Detected: " + guessTZ()}
|
|
||||||
onClick={() => setTimezone(guessTZ())}
|
|
||||||
>
|
|
||||||
Auto-detect
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Features</h2>
|
|
||||||
<p>Configure the features of the bot</p>
|
|
||||||
<label for="timePlanning" class="flex-row">
|
|
||||||
<p>Time Planning </p>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={
|
|
||||||
config.features.timePlanning.enabled ? faToggleOn : faToggleOff
|
|
||||||
}
|
|
||||||
size="xl"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
hidden
|
|
||||||
type="checkbox"
|
|
||||||
id="timePlanning"
|
|
||||||
ref={(e) => setTimePlanningRef(e)}
|
|
||||||
onInput={(e) =>
|
|
||||||
setConfig("features", "timePlanning", "enabled", e.target.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="sub"
|
|
||||||
classList={{ disabled: !config.features.timePlanning.enabled }}
|
|
||||||
>
|
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<label>Target channel:</label>
|
<label for="timezone">Timezone for your server:</label>
|
||||||
<select
|
<input
|
||||||
ref={(e) => setChannelRef(e)}
|
type="text"
|
||||||
|
list="timezones"
|
||||||
|
id="timezone"
|
||||||
|
ref={(e) => setTimezoneRef(e)}
|
||||||
onInput={(e) =>
|
onInput={(e) =>
|
||||||
setConfig(
|
setConfig(
|
||||||
"features",
|
"features",
|
||||||
"timePlanning",
|
"timePlanning",
|
||||||
"channelId",
|
"timezone",
|
||||||
e.target.value,
|
e.target.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
|
||||||
<option disabled value="">
|
|
||||||
--Select a Channel--
|
|
||||||
</option>
|
|
||||||
<For each={payload().guild.channels}>
|
|
||||||
{(channel) => (
|
|
||||||
<option value={channel.id}>{channel.name}</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex-row">
|
|
||||||
<label for="pingableRoles" class="flex-row">
|
|
||||||
<p>Enable pingable Roles:</p>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={
|
|
||||||
config.features.timePlanning.pingableRoles
|
|
||||||
? faToggleOn
|
|
||||||
: faToggleOff
|
|
||||||
}
|
|
||||||
size="xl"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
hidden
|
|
||||||
type="checkbox"
|
|
||||||
id="pingableRoles"
|
|
||||||
ref={(e) => setPingableRolesRef(e)}
|
|
||||||
onInput={(e) =>
|
|
||||||
setConfig(
|
|
||||||
"features",
|
|
||||||
"timePlanning",
|
|
||||||
"pingableRoles",
|
|
||||||
e.target.checked,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
<datalist id="timezones">
|
||||||
<button>Apply</button>
|
<Index each={payload()?.tzNames}>
|
||||||
<button onClick={() => navigator("/config")}>Back</button>
|
{(zone) => <option value={zone()} />}
|
||||||
</section>
|
</Index>
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={guessTZ() === config.features.timePlanning.timezone}
|
||||||
|
title={"Detected: " + guessTZ()}
|
||||||
|
onClick={() =>
|
||||||
|
setConfig("features", "timePlanning", "timezone", guessTZ())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Auto-detect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="box">
|
||||||
|
<h2>Features</h2>
|
||||||
|
<p>Configure the features of the bot</p>
|
||||||
|
<label for="timePlanning" class="flex-row">
|
||||||
|
<p>Time Planning </p>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
config.features.timePlanning.enabled
|
||||||
|
? faToggleOn
|
||||||
|
: faToggleOff
|
||||||
|
}
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
type="checkbox"
|
||||||
|
id="timePlanning"
|
||||||
|
ref={(e) => setTpEnabledRef(e)}
|
||||||
|
onInput={(e) =>
|
||||||
|
setConfig(
|
||||||
|
"features",
|
||||||
|
"timePlanning",
|
||||||
|
"enabled",
|
||||||
|
e.target.checked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="sub"
|
||||||
|
classList={{ disabled: !config.features.timePlanning.enabled }}
|
||||||
|
>
|
||||||
|
<div class="flex-row">
|
||||||
|
<label>Target channel:</label>
|
||||||
|
<select
|
||||||
|
ref={(e) => setChannelRef(e)}
|
||||||
|
onInput={(e) =>
|
||||||
|
setConfig(
|
||||||
|
"features",
|
||||||
|
"timePlanning",
|
||||||
|
"channelId",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option disabled value="">
|
||||||
|
--Select a Channel--
|
||||||
|
</option>
|
||||||
|
<For each={payload()?.guild.channels}>
|
||||||
|
{(channel) => (
|
||||||
|
<option value={channel.id}>{channel.name}</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
config.features.timePlanning.enabled &&
|
||||||
|
!config.features.timePlanning.channelId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{"<-- or changes won't be saved"}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
|
<label>Target Interval:</label>
|
||||||
|
<select
|
||||||
|
ref={(e) => setTargetDayRef(e)}
|
||||||
|
onInput={(e) =>
|
||||||
|
setConfig(
|
||||||
|
"features",
|
||||||
|
"timePlanning",
|
||||||
|
"targetDay",
|
||||||
|
Number(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Index each={Array.from(Array(7)).map((_e, i) => i)}>
|
||||||
|
{(id) => {
|
||||||
|
const weekdays = [
|
||||||
|
"Sunday",
|
||||||
|
"Monday",
|
||||||
|
"Tuesday",
|
||||||
|
"Wednesday",
|
||||||
|
"Thursday",
|
||||||
|
"Friday",
|
||||||
|
"Saturday",
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<option value={String(id())}>{weekdays[id()]}</option>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Index>
|
||||||
|
</select>
|
||||||
|
at
|
||||||
|
<select
|
||||||
|
ref={(e) => setTargetHourRef(e)}
|
||||||
|
onInput={(e) =>
|
||||||
|
setConfig(
|
||||||
|
"features",
|
||||||
|
"timePlanning",
|
||||||
|
"targetHour",
|
||||||
|
Number(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Index each={Array.from(Array(24)).map((_e, i) => i)}>
|
||||||
|
{(id) => (
|
||||||
|
<option value={String(id())}>{String(id())}</option>
|
||||||
|
)}
|
||||||
|
</Index>
|
||||||
|
</select>
|
||||||
|
:
|
||||||
|
<select
|
||||||
|
ref={(e) => setTargetMinuteRef(e)}
|
||||||
|
onInput={(e) =>
|
||||||
|
setConfig(
|
||||||
|
"features",
|
||||||
|
"timePlanning",
|
||||||
|
"targetMinute",
|
||||||
|
Number(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Index each={Array.from(Array(60)).map((_e, i) => i)}>
|
||||||
|
{(id) => (
|
||||||
|
<option value={String(id())}>
|
||||||
|
{String(id()).padStart(2, "0")}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
</Index>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
|
<label for="pingableRoles" class="flex-row">
|
||||||
|
<p>Enable pingable Roles:</p>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
config.features.timePlanning.pingableRoles
|
||||||
|
? faToggleOn
|
||||||
|
: faToggleOff
|
||||||
|
}
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
type="checkbox"
|
||||||
|
id="pingableRoles"
|
||||||
|
ref={(e) => setPingableRolesRef(e)}
|
||||||
|
onInput={(e) =>
|
||||||
|
setConfig(
|
||||||
|
"features",
|
||||||
|
"timePlanning",
|
||||||
|
"pingableRoles",
|
||||||
|
e.target.checked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="box">
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const id = payload()?.guild.id;
|
||||||
|
if (!id || !willUpdateValues()) return;
|
||||||
|
await saveConfig(id, updateValues());
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
<button onClick={() => navigator("/config")}>Back</button>
|
||||||
|
<Show when={willUpdateValues()}>UNSAVED CHANGES</Show>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { faPlusCircle, faWrench } from "@fortawesome/pro-regular-svg-icons";
|
import { faPlusCircle, faWrench } from "@fortawesome/pro-regular-svg-icons";
|
||||||
import { useLocation, useNavigate } from "@solidjs/router";
|
import { useLocation, useNavigate } from "@solidjs/router";
|
||||||
import createClient from "openapi-fetch";
|
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 { getRequestEvent } from "solid-js/web";
|
||||||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||||
import Layout from "~/components/Layout";
|
import Layout from "~/components/Layout";
|
||||||
|
import db from "~/drizzle";
|
||||||
|
import { guilds } from "~/drizzle/schema";
|
||||||
import getAccessToken from "~/lib/accessToken";
|
import getAccessToken from "~/lib/accessToken";
|
||||||
import { paths } from "~/types/discord";
|
import { paths } from "~/types/discord";
|
||||||
import "../../styles/pages/config.scss";
|
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")
|
if (typeof import.meta.env.VITE_DISCORD_OAUTH2_PERMISSIONS === "undefined")
|
||||||
throw new Error("No env VITE_DISCORD_OAUTH2_PERMISSIONS found!");
|
throw new Error("No env VITE_DISCORD_OAUTH2_PERMISSIONS found!");
|
||||||
|
|
||||||
|
interface Guild {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const initialValue = () => ({
|
const initialValue = () => ({
|
||||||
success: null as boolean | null,
|
success: null as boolean | null,
|
||||||
guilds: [] as {
|
joined: [] as Guild[],
|
||||||
id: string;
|
canJoin: [] as Guild[],
|
||||||
name: string;
|
|
||||||
icon: string | null | undefined;
|
|
||||||
}[],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const getPayload = async (): Promise<
|
const getPayload = async (): Promise<
|
||||||
|
@ -43,9 +48,10 @@ const getPayload = async (): Promise<
|
||||||
const { GET } = createClient<paths>({
|
const { GET } = createClient<paths>({
|
||||||
baseUrl: "https://discord.com/api/v10",
|
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}` },
|
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||||
});
|
});
|
||||||
|
const configs = await db.select().from(guilds).execute();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log("Discord api error:", { error });
|
console.log("Discord api error:", { error });
|
||||||
|
@ -53,12 +59,21 @@ const getPayload = async (): Promise<
|
||||||
}
|
}
|
||||||
console.log(pathname, "success");
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
guilds:
|
joined,
|
||||||
guilds
|
canJoin,
|
||||||
?.filter((e) => parseInt(e.permissions) & (1 << 5))
|
|
||||||
.map(({ id, name, icon }) => ({ id, name, icon })) ?? [],
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,64 +99,62 @@ function index() {
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
},
|
},
|
||||||
{ deferStream: true },
|
{ deferStream: true, initialValue: initialValue() },
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout site="config">
|
<Layout site="config">
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<h3>Configure li'l Judd in</h3>
|
<h3>Configure li'l Judd in</h3>
|
||||||
<div>
|
<GuildList list={payload().joined} joined={true} />
|
||||||
<For each={payload()?.guilds}>
|
|
||||||
{(guild) => {
|
|
||||||
return (
|
|
||||||
<a href={`/config/${guild.id}`} class="flex-row centered">
|
|
||||||
<img
|
|
||||||
class="guildpfp"
|
|
||||||
src={
|
|
||||||
guild.icon
|
|
||||||
? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=240`
|
|
||||||
: "https://cdn.discordapp.com/icons/1040502664506646548/bb5a51c4659cf47bdd942bb11e974da7.webp?size=240"
|
|
||||||
}
|
|
||||||
alt="Server pfp"
|
|
||||||
/>
|
|
||||||
<h1>{guild.name}</h1>
|
|
||||||
<FontAwesomeIcon icon={faWrench} size="xl" />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<h3>Add li'l Judd to</h3>
|
<h3>Add li'l Judd to</h3>
|
||||||
<div>
|
<GuildList list={payload().canJoin} joined={false} />
|
||||||
<For each={payload()?.guilds}>
|
|
||||||
{(guild) => {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={`https://discord.com/api/oauth2/authorize?client_id=${import.meta.env.VITE_DISCORD_CLIENT_ID}&permissions=${import.meta.env.VITE_DISCORD_OAUTH2_PERMISSIONS}&scope=bot&guild_id=${guild.id}`}
|
|
||||||
class="flex-row centered"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="guildpfp"
|
|
||||||
src={
|
|
||||||
guild.icon
|
|
||||||
? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=240`
|
|
||||||
: "https://cdn.discordapp.com/icons/1040502664506646548/bb5a51c4659cf47bdd942bb11e974da7.webp?size=240"
|
|
||||||
}
|
|
||||||
alt="Server pfp"
|
|
||||||
/>
|
|
||||||
<h1>{guild.name}</h1>
|
|
||||||
<FontAwesomeIcon icon={faPlusCircle} size="xl" />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GuildList(props: { list: Guild[]; joined: boolean }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Show
|
||||||
|
when={props.list.length}
|
||||||
|
fallback={<div class="box flex-row centered">Nothing here</div>}
|
||||||
|
>
|
||||||
|
<For each={props.list}>
|
||||||
|
{(guild) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
props.joined
|
||||||
|
? `/config/${guild.id}`
|
||||||
|
: `https://discord.com/api/oauth2/authorize?client_id=${import.meta.env.VITE_DISCORD_CLIENT_ID}&permissions=${import.meta.env.VITE_DISCORD_OAUTH2_PERMISSIONS}&scope=bot&guild_id=${guild.id}`
|
||||||
|
}
|
||||||
|
class="box effect flex-row centered"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="guildpfp"
|
||||||
|
src={
|
||||||
|
guild.icon
|
||||||
|
? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=240`
|
||||||
|
: "https://cdn.discordapp.com/icons/1040502664506646548/bb5a51c4659cf47bdd942bb11e974da7.webp?size=240"
|
||||||
|
}
|
||||||
|
alt="Server pfp"
|
||||||
|
/>
|
||||||
|
<h1>{guild.name}</h1>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={props.joined ? faWrench : faPlusCircle}
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default index;
|
export default index;
|
||||||
|
|
|
@ -16,8 +16,7 @@
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
section,
|
.box {
|
||||||
a {
|
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
transition-timing-function: ease-out;
|
transition-timing-function: ease-out;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
@ -28,28 +27,30 @@
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
svg {
|
&.effect {
|
||||||
padding: 0 1rem;
|
svg {
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
path {
|
path {
|
||||||
transition: 0.1s;
|
transition: 0.1s;
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transition: 1s;
|
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;
|
|
||||||
transition-delay: 0.1s;
|
transition-delay: 0.1s;
|
||||||
transition-timing-function: ease-out;
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
transform: translateX(0);
|
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
opacity: 1;
|
|
||||||
|
svg path {
|
||||||
|
transition: 0.5s;
|
||||||
|
transition-delay: 0.1s;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue