style: login and config working

This commit is contained in:
Aron Malcher 2024-01-16 20:22:23 +01:00
parent a657906f4f
commit 55b81fac91
Signed by: aronmal
GPG key ID: 816B7707426FC612
28 changed files with 2322 additions and 3469 deletions

View file

@ -13,10 +13,18 @@
"@auth/core": "^0.19.0", "@auth/core": "^0.19.0",
"@auth/drizzle-adapter": "^0.3.12", "@auth/drizzle-adapter": "^0.3.12",
"@auth/solid-start": "0.1.2", "@auth/solid-start": "0.1.2",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/pro-duotone-svg-icons": "^6.5.1",
"@fortawesome/pro-light-svg-icons": "^6.5.1",
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
"@fortawesome/pro-thin-svg-icons": "^6.5.1",
"@fortawesome/sharp-solid-svg-icons": "^6.5.1",
"@solidjs/meta": "^0.29.2", "@solidjs/meta": "^0.29.2",
"@solidjs/router": "^0.10.9", "@solidjs/router": "^0.10.9",
"@solidjs/start": "^0.4.9", "@solidjs/start": "^0.4.9",
"drizzle-orm": "^0.29.2", "drizzle-orm": "^0.29.2",
"moment-timezone": "^0.5.44",
"postgres": "^3.4.3", "postgres": "^3.4.3",
"solid-js": "^1.8.11", "solid-js": "^1.8.11",
"vinxi": "^0.1.2" "vinxi": "^0.1.2"

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,10 @@
// @refresh reload // @refresh reload
import "@fortawesome/fontawesome-svg-core/styles.css";
import { Meta, MetaProvider, Title } from "@solidjs/meta"; import { Meta, MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router"; import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start"; import { FileRoutes } from "@solidjs/start";
import { Suspense } from "solid-js"; import { Suspense } from "solid-js";
import Footer from "./components/Footer"; import "./styles/global.scss";
import NavBar from "./components/NavBar";
import "./styles/GlobalLayout.css";
import "./styles/Layout.scss";
export default function App() { export default function App() {
return ( return (
@ -18,9 +16,8 @@ export default function App() {
content="The Splatoon Discord bot with unique features." content="The Splatoon Discord bot with unique features."
/> />
<Title>li'l Judd - Your competitive Splatoon assistant</Title> <Title>li'l Judd - Your competitive Splatoon assistant</Title>
<NavBar />
<Suspense>{props.children}</Suspense> <Suspense>{props.children}</Suspense>
<Footer />
</MetaProvider> </MetaProvider>
)} )}
> >

View file

@ -0,0 +1,151 @@
import {
FaSymbol,
FlipProp,
IconDefinition,
IconProp,
PullProp,
RotateProp,
SizeProp,
Transform,
} from "@fortawesome/fontawesome-svg-core";
import { type JSX } from "solid-js";
export interface FontAwesomeIconProps
extends Omit<
JSX.SvgSVGAttributes<SVGSVGElement>,
"children" | "mask" | "transform"
> {
icon: IconDefinition;
mask?: IconProp;
maskId?: string;
color?: string;
spin?: boolean;
spinPulse?: boolean;
spinReverse?: boolean;
pulse?: boolean;
beat?: boolean;
fade?: boolean;
beatFade?: boolean;
bounce?: boolean;
shake?: boolean;
flash?: boolean;
border?: boolean;
fixedWidth?: boolean;
inverse?: boolean;
listItem?: boolean;
flip?: FlipProp;
size?: SizeProp;
pull?: PullProp;
rotation?: RotateProp;
transform?: string | Transform;
symbol?: FaSymbol;
style?: JSX.CSSProperties;
tabIndex?: number;
title?: string;
titleId?: string;
swapOpacity?: boolean;
}
const idPool = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
function nextUniqueId() {
let size = 12;
let id = "";
while (size-- > 0) {
id += idPool[(Math.random() * 62) | 0];
}
return id;
}
function Path(props: { d: string | string[] }) {
return (
<>
{typeof props.d === "string" ? (
<path fill="currentColor" d={props.d} />
) : (
<>
<path class="fa-secondary" fill="currentColor" d={props.d[0]} />
<path class="fa-primary" fill="currentColor" d={props.d[1]} />
</>
)}
</>
);
}
export function FontAwesomeIcon(props: FontAwesomeIconProps) {
const titleId = () =>
props.title
? "svg-inline--fa-title-".concat(props.titleId || nextUniqueId())
: undefined;
// Get CSS class list from the props object
function attributes() {
const defaultClasses = {
"svg-inline--fa": true,
[`fa-${props.icon.iconName}`]: true,
[props.class ?? ""]:
typeof props.class !== "undefined" && props.class !== null,
...props.classList,
};
// map of CSS class names to properties
const faClasses = {
"fa-beat": props.beat,
"fa-fade": props.fade,
"fa-beat-fade": props.beatFade,
"fa-bounce": props.bounce,
"fa-shake": props.shake,
"fa-flash": props.flash,
"fa-spin": props.spin,
"fa-spin-reverse": props.spinReverse,
"fa-spin-pulse": props.spinPulse,
"fa-pulse": props.pulse,
"fa-fw": props.fixedWidth,
"fa-inverse": props.inverse,
"fa-border": props.border,
"fa-li": props.listItem,
"fa-flip": typeof props.flip !== "undefined" && props.flip !== null,
"fa-flip-horizontal":
props.flip === "horizontal" || props.flip === "both",
"fa-flip-vertical": props.flip === "vertical" || props.flip === "both",
[`fa-${props.size}`]:
typeof props.size !== "undefined" && props.size !== null,
[`fa-rotate-${props.rotation}`]:
typeof props.rotation !== "undefined" && props.size !== null,
[`fa-pull-${props.pull}`]:
typeof props.pull !== "undefined" && props.pull !== null,
"fa-swap-opacity": props.swapOpacity,
};
const attributes = {
focusable: !!props.title,
"aria-hidden": !props.title,
role: "img",
xmlns: "http://www.w3.org/2000/svg",
"aria-labelledby": titleId(),
"data-prefix": props.icon.prefix,
"data-icon": props.icon.iconName,
"data-fa-transform": props.transform,
"data-fa-mask": props.mask,
"data-fa-mask-id": props.maskId,
"data-fa-symbol": props.symbol,
tabIndex: props.tabIndex,
classList: { ...defaultClasses, ...faClasses },
color: props.color,
style: props.style,
viewBox: `0 0 ${props.icon.icon[0]} ${props.icon.icon[1]}`,
} as const;
// return the complete class list
return attributes;
}
return (
<svg {...attributes()}>
{/* <Show when={props.title}>
<title id={titleId()}>{props.title}</title>
</Show> */}
<Path d={props.icon.icon[4]} />
</svg>
);
}

18
src/components/Layout.tsx Normal file
View file

@ -0,0 +1,18 @@
import { JSX, Suspense } from "solid-js";
import "../styles/Layout.scss";
import Footer from "./Footer";
import NavBar from "./NavBar";
function Layout(props: { children: JSX.Element; site: string }) {
return (
<>
<NavBar />
<div class={props.site}>
<Suspense>{props.children}</Suspense>
</div>
<Footer />
</>
);
}
export default Layout;

View file

@ -1,35 +1,53 @@
import { faCirclePlus } from "@fortawesome/pro-regular-svg-icons";
import { JSX, Show, Suspense } from "solid-js";
import "../styles/components/NavBar.scss"; import "../styles/components/NavBar.scss";
import { FontAwesomeIcon } from "./FontAwesomeIcon";
import NavUser from "./NavUser";
export function Li(props: {
href: string;
action?: () => void;
name?: string;
children?: JSX.Element;
}) {
return (
<li class="navElem flex-row thick">
<a
class="flex-row"
href={props.href}
onClick={() => props.action && props.action()}
>
{props.children ?? <></>}
<Show when={props.name}>
<span>{props.name}</span>
</Show>
</a>
</li>
);
}
function NavBar() { function NavBar() {
return ( return (
<nav> <nav class="flex-row responsive">
<ul> <ul class="flex-row responsive thick">
<li class="navElem"> <Li href="/" name="li'l Judd">
<a class="textBx" href="/"> <img src="/assets/logox256.png" alt="The Bots Logo" />
<img id="logo" src="/assets/logox256.png" alt="The Bots Logo" /> </Li>
li&apos;l Judd <Li href="/features" name="Features" />
</a> <Li href="/how-do-i" name="How do I...?" />
</li> <Li href="/stack" name="The Stack" />
<li class="navElem"> <Li href="/about" name="About" />
<a href="/features">Features</a> </ul>
</li> <ul class="flex-row responsive thick">
<li class="navElem"> <Li
<a href="/how-do-i">How do I...?</a>
</li>
<li class="navElem">
<a href="/stack">The Stack</a>
</li>
<li class="navElem">
<a href="/about">About</a>
</li>
<li class="navElem">
<a
href="https://discord.com/api/oauth2/authorize?client_id=1024410658973941862&permissions=18977581952080&scope=bot" href="https://discord.com/api/oauth2/authorize?client_id=1024410658973941862&permissions=18977581952080&scope=bot"
target="_blank" name="Invite to your server"
> >
Invite to your server <FontAwesomeIcon class="lower" icon={faCirclePlus} size="xl" />
</a> </Li>
</li> <Suspense>
<NavUser />
</Suspense>
</ul> </ul>
</nav> </nav>
); );

View file

@ -0,0 +1,87 @@
import { getSession } from "@auth/solid-start";
import { signIn, signOut } from "@auth/solid-start/client";
import {
faArrowRightFromBracket,
faArrowRightToBracket,
faGear,
} from "@fortawesome/pro-regular-svg-icons";
import { eq } from "drizzle-orm";
import { Show, createResource } from "solid-js";
import { getRequestEvent } from "solid-js/web";
import db from "~/drizzle";
import { users } from "~/drizzle/schema";
import { authOptions } from "~/server/auth";
import { FontAwesomeIcon } from "./FontAwesomeIcon";
import { Li } from "./NavBar";
const initialUser = {
id: "",
name: null as string | null,
email: "",
emailVerified: null as Date | null,
image: null as string | null,
};
async function getUser() {
"use server";
const event = getRequestEvent();
if (!event)
return { success: false, message: "No request event!", ...initialUser };
const session = await getSession(event.request, authOptions);
if (!session?.user?.id)
return { success: false, message: "No user with id!", ...initialUser };
const user = (
await db
.selectDistinct()
.from(users)
.where(eq(users.id, session.user?.id))
.limit(1)
.execute()
)[0];
return { success: true, message: "", ...user };
}
function NavUser() {
const [user] = createResource(() =>
getUser().then((e) => {
if (!e.success) console.log(1, e.message);
console.log(2, e);
return e;
}),
);
return (
<Show
when={user()?.id}
fallback={
<Li
href="#"
name="Login"
action={() => signIn("discord", { callbackUrl: "/config" })}
>
<FontAwesomeIcon
class="secondary"
icon={faArrowRightToBracket}
size="xl"
/>
</Li>
}
>
<Li href="/config">
<div class="swap lower">
<img class="primary" src={user()?.image ?? ""} alt="User pfp" />
<FontAwesomeIcon class="secondary" icon={faGear} size="xl" />
</div>
</Li>
<Li href="#" action={() => signOut({ callbackUrl: "/" })}>
<FontAwesomeIcon icon={faArrowRightFromBracket} size="xl" />
</Li>
</Show>
);
}
export default NavUser;

View file

@ -1,10 +1,11 @@
import { Title } from "@solidjs/meta"; import { Title } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start"; import { HttpStatusCode } from "@solidjs/start";
import Layout from "~/components/Layout";
import "../styles/pages/index.scss"; import "../styles/pages/index.scss";
export default function NotFound() { export default function NotFound() {
return ( return (
<> <Layout site="index">
<Title>Not Found</Title> <Title>Not Found</Title>
<HttpStatusCode code={404} /> <HttpStatusCode code={404} />
<section class="index"> <section class="index">
@ -17,6 +18,6 @@ export default function NotFound() {
</p> </p>
</div> </div>
</section> </section>
</> </Layout>
); );
} }

View file

@ -1,9 +1,9 @@
import Layout from "~/components/Layout";
import "../styles/pages/about.scss"; import "../styles/pages/about.scss";
function about() { function about() {
return ( return (
<> <Layout site="about">
<div class="aboutdiv">
<h1>About</h1> <h1>About</h1>
<section> <section>
<h2>Why does this bot exist?</h2> <h2>Why does this bot exist?</h2>
@ -12,19 +12,19 @@ function about() {
<a href="/assets/screenshots/oldplanningmsg.png" target="_blank"> <a href="/assets/screenshots/oldplanningmsg.png" target="_blank">
these planning messages these planning messages
</a>{" "} </a>{" "}
and I thought that this should be automated. Some time later the and I thought that this should be automated. Some time later the first
first version of li'l Judd was born. Today the bot has more features version of li'l Judd was born. Today the bot has more features and
and keeps getting more of them! It is designed to actually improve keeps getting more of them! It is designed to actually improve the
the Splatoon experience and not be the 10000th moderation and Splatoon experience and not be the 10000th moderation and general
general utility bot with the same features as all bots. utility bot with the same features as all bots.
</p> </p>
</section> </section>
<section> <section>
<h2>Who is behind this?</h2> <h2>Who is behind this?</h2>
<p> <p>
The bot is currently being developed by{" "} The bot is currently being developed by{" "}
<a href="/contact">moonleay</a> (hey that&apos;s me!) with <a href="/contact">moonleay</a> (hey that&apos;s me!) with occasional
occasional help from his friends! help from his friends!
</p> </p>
</section> </section>
<section> <section>
@ -36,17 +36,17 @@ function about() {
<a href="https://git.moonleay.net/DiscordBots/lilJudd"> <a href="https://git.moonleay.net/DiscordBots/lilJudd">
read the code read the code
</a> </a>
and if you still don't trust me, you can always host the bot and if you still don't trust me, you can always host the bot yourself!
yourself! A guide on how to do that can be found in the README of A guide on how to do that can be found in the README of the git
the git project. project.
</p> </p>
</section> </section>
<section> <section>
<h2>Where is my data stored?</h2> <h2>Where is my data stored?</h2>
<p> <p>
Your data is stored on a VPS from Contabo in Germany. The bot used Your data is stored on a VPS from Contabo in Germany. The bot used to
to be hosted on a server in my basement, but I moved it to a VPS, be hosted on a server in my basement, but I moved it to a VPS, because
because my internet connection was not stable enough. my internet connection was not stable enough.
</p> </p>
</section> </section>
<section> <section>
@ -66,13 +66,12 @@ function about() {
<section> <section>
<h2>Hey, there is this really cool idea I have! Can you add it?</h2> <h2>Hey, there is this really cool idea I have! Can you add it?</h2>
<p> <p>
Just message me! I can't promise anything, but I am always open to Just message me! I can't promise anything, but I am always open to new
new ideas and improvements! You can find ways to contact me{" "} ideas and improvements! You can find ways to contact me{" "}
<a href="/contact">here</a>. <a href="/contact">here</a>.
</p> </p>
</section> </section>
</div> </Layout>
</>
); );
} }

View file

@ -1,9 +1,9 @@
import Layout from "~/components/Layout";
import "../styles/pages/acknowledgements.scss"; import "../styles/pages/acknowledgements.scss";
function acknowledgements() { function acknowledgements() {
return ( return (
<> <Layout site="acknowledgements">
<div class="acknowledgements">
<h1>Acknowledgements</h1> <h1>Acknowledgements</h1>
<section> <section>
<table> <table>
@ -142,10 +142,7 @@ function acknowledgements() {
</a> </a>
</td> </td>
<td> <td>
<a <a href="https://github.com/JetBrains/Exposed" target="_blank">
href="https://github.com/JetBrains/Exposed"
target="_blank"
>
repo repo
</a> </a>
</td> </td>
@ -192,8 +189,7 @@ function acknowledgements() {
</tbody> </tbody>
</table> </table>
</section> </section>
</div> </Layout>
</>
); );
} }

View file

@ -1,60 +1,270 @@
import "../styles/pages/config.scss"; import { getSession } from "@auth/solid-start";
import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons";
import { useNavigate, useParams } from "@solidjs/router";
import { eq } from "drizzle-orm";
import moment from "moment-timezone";
import createClient from "openapi-fetch";
import { Index, createEffect, createResource, createSignal } from "solid-js";
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 { accounts } from "~/drizzle/schema";
import { authOptions } from "~/server/auth";
import { paths } from "~/types/discord";
import "../../styles/pages/config.scss";
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,
},
tzNames: [guessTZ()],
});
const getPayload = async (
id: string,
): Promise<
| { success: false; message: string }
| (ReturnType<typeof initialValue> & { success: true })
> => {
"use server";
const event = getRequestEvent();
if (!event) return { success: false, message: "No request event!" };
const session = await getSession(event.request, authOptions);
if (!session?.user?.id)
return { success: false, message: "No user with id!" };
const { DISCORD_ACCESS_TOKEN } = (
await db
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
.from(accounts)
.where(eq(accounts.userId, session.user?.id))
.limit(1)
.execute()
)[0];
if (!DISCORD_ACCESS_TOKEN)
return { success: false, message: "No discord access token!" };
// const guilds = await fetch("https://discord.com/api/users/@me/guilds", {
// headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
// }).then((res) => res.json());
const { GET } = createClient<paths>({
baseUrl: "https://discord.com/api/v10",
});
const { data: guilds, error } = await GET("/users/@me/guilds", {
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
});
if (error) {
console.log(error);
return { success: false, message: "Error on discord api request!" };
}
const guild = guilds?.find((e) => e.id === id);
if (!guild)
return {
success: false,
message: "User is in no such guild with requested id!",
};
if (!(parseInt(guild.permissions) & (1 << 5)))
return {
success: false,
message:
"User is no MANAGE_GUILD permissions on this guild with requested id!",
};
return {
success: true,
guild: {
id: guild.id,
name: guild.name,
icon: guild.icon,
},
// guild: guilds
// .filter((e: any) => e.permissions & (1 << 5))
// .map((e: any) => e.name),
tzNames: moment.tz.names(),
};
};
function config() { function config() {
const params = useParams();
const navigator = useNavigate();
let timezoneRef: HTMLInputElement;
let timePlanningRef: HTMLInputElement;
let pingableRolesRef: HTMLInputElement;
const [timezone, setTimezone] = createSignal(guessTZ());
const [payload] = createResource(params.guildId, async (id) => {
const payload = await getPayload(id);
if (!payload.success) {
console.log(payload.message, "No success");
navigator("/config", { replace: false });
return initialValue(params);
}
return payload;
});
const guild = () => payload()?.guild ?? initialValue(params).guild;
const tzNames = () => payload()?.tzNames ?? [];
const [config, setConfig] = createStore({
features: {
timePlanning: {
enabled: false,
pingableRoles: false,
},
},
});
createEffect(() => console.log(payload()));
createEffect(() => console.log("timezone", timezone()));
createEffect(() =>
console.log("timePlanning.enabled", config.features.timePlanning.enabled),
);
createEffect(() =>
console.log(
"timePlanning.pingableRoles",
config.features.timePlanning.pingableRoles,
),
);
createEffect(() => (timezoneRef.value = timezone()));
createEffect(
() => (timePlanningRef.checked = config.features.timePlanning.enabled),
);
createEffect(
() =>
(pingableRolesRef.checked = config.features.timePlanning.pingableRoles),
);
return ( return (
<Layout site="config">
<h3 class="text-center">Configure li&apos;l Judd in</h3>
<div> <div>
<h3 class={"centered"}>Configure li&apos;l Judd in</h3>
<div class={"config"}>
<div> <div>
<div class={"horizontal centered"}> <div class="flex-row centered">
<img <img
class={"guildpfp"} class="guildpfp"
src="https://cdn.discordapp.com/icons/1040502664506646548/bb5a51c4659cf47bdd942bb11e974da7.webp?size=240" 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" alt="Server pfp"
/> />
<h1>li'l Judds home base</h1> <h1>{guild()?.name ?? "li'l Judds home base"}</h1>
</div> </div>
</div> </div>
<article>
<h2>Timezone</h2>
<p>Set the timezone for your server.</p>
<label>
<select>
<option value="-1">UTC-1:00</option>
<option value="0">UTC+0:00</option>
<option value="1">UTC+1:00</option>
</select>
</label>
</article>
<section> <section>
<div class={"centered"}> <h2>Guild</h2>
<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={timezoneRef!}
// disabled={!tzNames().find((e) => e === timezone())}
onInput={(e) => setTimezone(e.target.value)}
/>
<datalist id="timezones">
<Index each={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> <h2>Features</h2>
<p>Configure the features of the bot</p> <p>Configure the features of the bot</p>
</div> <label for="timePlanning" class="flex-row">
<article> <p>Time Planning </p>
<div class={"horizontal"}> <FontAwesomeIcon
<h3>Time Planning</h3> icon={
<input type="checkbox" id="time planning" /> config.features.timePlanning.enabled ? faToggleOn : faToggleOff
</div> }
<label class={"horizontal"}> size="xl"
<p class={"marg_right_5px"}>Target channel:</p> />
<select>
<option value="id">Channel 1</option>
<option value="id">Channel 2</option>
<option value="id">Channel 3</option>
<option value="id">Channel 4</option>
</select>
</label> </label>
<div class={"horizontal"}> <input
<h4>Enable pingable Roles</h4> hidden
<input type="checkbox" id="pingableroles" /> type="checkbox"
id="timePlanning"
ref={timePlanningRef!}
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 value={timezone()}>
<optgroup label="--Select a Channel--">
<Index each={tzNames()}>
{(channel) => <option>{channel()}</option>}
</Index>
</optgroup>
</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={pingableRolesRef!}
onInput={(e) =>
setConfig(
"features",
"timePlanning",
"pingableRoles",
e.target.checked,
)
}
/>
</div>
</div> </div>
</article>
</section> </section>
<section>
<button>Apply</button> <button>Apply</button>
</section>
</div> </div>
</div> </Layout>
); );
} }

124
src/routes/config/index.tsx Normal file
View file

@ -0,0 +1,124 @@
import { getSession } from "@auth/solid-start";
import {
faBadgeCheck,
faCircleExclamation,
faPlus,
} from "@fortawesome/pro-regular-svg-icons";
import { useNavigate } from "@solidjs/router";
import { eq } from "drizzle-orm";
import createClient from "openapi-fetch";
import { For, 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 { accounts } from "~/drizzle/schema";
import { authOptions } from "~/server/auth";
import { paths } from "~/types/discord";
import "../../styles/pages/config.scss";
const initialValue = () => ({
success: null as boolean | null,
guilds: [] as {
id: string;
name: string;
icon: string | null | undefined;
}[],
});
const getPayload = async (): Promise<
| { success: false; message: string }
| (ReturnType<typeof initialValue> & { success: true })
> => {
("use server");
const event = getRequestEvent();
if (!event) return { success: false, message: "No request event!" };
const session = await getSession(event.request, authOptions);
if (!session?.user?.id)
return { success: false, message: "No user with id!" };
const { DISCORD_ACCESS_TOKEN } = (
await db
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
.from(accounts)
.where(eq(accounts.userId, session.user?.id))
.limit(1)
.execute()
)[0];
if (!DISCORD_ACCESS_TOKEN)
return { success: false, message: "No discord access token!" };
const { GET } = createClient<paths>({
baseUrl: "https://discord.com/api/v10",
});
const { data: guilds, error } = await GET("/users/@me/guilds", {
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
});
console.log("guilds", guilds);
if (error) {
console.log(error);
return { success: false, message: "Error on discord api request!" };
}
return {
success: true,
guilds:
guilds
?.filter((e) => parseInt(e.permissions) & (1 << 5))
.map(({ id, name, icon }) => ({ id, name, icon })) ?? [],
};
};
function index() {
const navigator = useNavigate();
const [payload] = createResource(async () => {
const payload = await getPayload();
if (!payload.success) {
console.log(payload.message, "No success");
navigator("/", { replace: false });
return initialValue();
}
console.log("success");
return payload;
});
const icons = [faPlus, faCircleExclamation, faBadgeCheck];
const colors = [undefined, "orange", "green"];
return (
<Layout site="config">
<h3 class="text-center">Configure li&apos;l Judd in</h3>
<div>
<For each={payload()?.guilds ?? []}>
{(guild, i) => (
<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
// beat={i() % 3 === 1}
color={colors[i() % 3]}
icon={icons[i() % 3]}
size="xl"
/>
</a>
)}
</For>
</div>
</Layout>
);
}
export default index;

View file

@ -1,19 +1,16 @@
import Layout from "~/components/Layout";
import "../styles/pages/contact.scss"; import "../styles/pages/contact.scss";
function contact() { function contact() {
return ( return (
<> <Layout site="contact">
<div class="contact">
<h1>Contact</h1> <h1>Contact</h1>
<section class="contact"> <section>
<a href="mailto:contact@moonleay.net" target="_blank"> <a href="mailto:contact@moonleay.net" target="_blank">
<img src="/assets/icons/email.svg" alt="Email" /> <img src="/assets/icons/email.svg" alt="Email" />
contact@moonleay.net contact@moonleay.net
</a> </a>
<a <a href="https://discord.com/users/372703841151614976" target="_blank">
href="https://discord.com/users/372703841151614976"
target="_blank"
>
<img src="/assets/icons/discord.svg" alt="Discord" /> <img src="/assets/icons/discord.svg" alt="Discord" />
@moonleay @moonleay
</a> </a>
@ -22,8 +19,7 @@ function contact() {
li'l Judd's home base li'l Judd's home base
</a> </a>
</section> </section>
</div> </Layout>
</>
); );
} }

View file

@ -1,11 +1,11 @@
import ImageSection from "~/components/ImageSection"; import ImageSection from "~/components/ImageSection";
import Layout from "~/components/Layout";
import "../styles/pages/features.scss"; import "../styles/pages/features.scss";
function features() { function features() {
return ( return (
<> <Layout site="features">
<div class="features"> <h1>Features</h1>
<h1 class="title">Features</h1>
<div class="gridlayout"> <div class="gridlayout">
<ImageSection <ImageSection
imgUrl="/assets/screenshots/timeplanner.png" imgUrl="/assets/screenshots/timeplanner.png"
@ -44,8 +44,7 @@ function features() {
note="If you have a specific feature request, you can contact me on Discord: @moonleay or email: contact at moonleay dot net" note="If you have a specific feature request, you can contact me on Discord: @moonleay or email: contact at moonleay dot net"
/> />
</div> </div>
</div> </Layout>
</>
); );
} }

View file

@ -1,10 +1,11 @@
import Layout from "~/components/Layout";
import "../styles/pages/how-do-i.scss"; import "../styles/pages/how-do-i.scss";
function howDoI() { function howDoI() {
return ( return (
<> <Layout site="how-do-i">
<h1 class="hdi-title">How do I...?</h1> <h1>How do I...?</h1>
<section class="hdi-section"> <section>
<h2>.. enable / disable certain features?</h2> <h2>.. enable / disable certain features?</h2>
<p> <p>
Features can be enabled and disables using the <code>/feature</code> Features can be enabled and disables using the <code>/feature</code>
@ -26,7 +27,7 @@ function howDoI() {
{/* <p><code>/feature feature:Time Planning Feature set:Enable channel:#ich-kann-heute</code></p> */} {/* <p><code>/feature feature:Time Planning Feature set:Enable channel:#ich-kann-heute</code></p> */}
</div> </div>
</section> </section>
<section class="hdi-section"> <section>
<h2>.. create a match?</h2> <h2>.. create a match?</h2>
<p> <p>
You can create a match time using the <code>/match</code> command. You can create a match time using the <code>/match</code> command.
@ -47,14 +48,14 @@ function howDoI() {
{/* <p><code>/match match:Ladder Match timestamp:24.12.2069 04:20 opponent:Forbidden</code></p> */} {/* <p><code>/match match:Ladder Match timestamp:24.12.2069 04:20 opponent:Forbidden</code></p> */}
</div> </div>
</section> </section>
<section class="hdi-footernotesection"> <section class="note">
<p> <p>
Is something missing here? Is something missing here?
<br /> <br />
Please <a href="/contact">contact me</a>! Please <a href="/contact">contact me</a>!
</p> </p>
</section> </section>
</> </Layout>
); );
} }

View file

@ -1,9 +1,10 @@
import Layout from "~/components/Layout";
import "../styles/pages/imprint.scss"; import "../styles/pages/imprint.scss";
function imprint() { function imprint() {
return ( return (
<> <Layout site="imprint">
<section class="imprint"> <section>
<h1>Imprint</h1> <h1>Imprint</h1>
<section> <section>
<a href="/contact"> <a href="/contact">
@ -54,7 +55,7 @@ function imprint() {
</h5> </h5>
</section> </section>
</section> </section>
</> </Layout>
); );
} }

View file

@ -1,9 +1,10 @@
import Layout from "~/components/Layout";
import "../styles/pages/index.scss"; import "../styles/pages/index.scss";
function index() { function index() {
return ( return (
<> <Layout site="index">
<section class="index"> <section>
<h1>li&apos;l Judd</h1> <h1>li&apos;l Judd</h1>
<h5>The competetive Splatoon Bot</h5> <h5>The competetive Splatoon Bot</h5>
<div> <div>
@ -13,7 +14,7 @@ function index() {
</p> </p>
</div> </div>
</section> </section>
</> </Layout>
); );
} }

View file

@ -1,9 +1,9 @@
import Layout from "~/components/Layout";
import "../styles/pages/privacy-policy.scss"; import "../styles/pages/privacy-policy.scss";
function privacyPolicy() { function privacyPolicy() {
return ( return (
<> <Layout site="privacyPolicy">
<div class="privacyPolicy">
<div> <div>
<h1>Privacy Policy for li&apos;l Judd</h1> <h1>Privacy Policy for li&apos;l Judd</h1>
<h4>Last updated: 2023-12-05</h4> <h4>Last updated: 2023-12-05</h4>
@ -57,8 +57,8 @@ function privacyPolicy() {
</ul> </ul>
<h3>3.2 Usage Data</h3> <h3>3.2 Usage Data</h3>
<p> <p>
We may collect information on how you interact with our bot, We may collect information on how you interact with our bot, including
including but not limited to: but not limited to:
</p> </p>
<ul> <ul>
<li> <li>
@ -96,8 +96,8 @@ function privacyPolicy() {
<ul> <ul>
<li> <li>
<p> <p>
- Consent: You have given your consent for the processing of - Consent: You have given your consent for the processing of your
your personal data for one or more specific purposes. personal data for one or more specific purposes.
</p> </p>
</li> </li>
<li> <li>
@ -111,9 +111,8 @@ function privacyPolicy() {
<section> <section>
<h2>6. Data Sharing</h2> <h2>6. Data Sharing</h2>
<p> <p>
We do not sell, trade, or otherwise transfer your personal We do not sell, trade, or otherwise transfer your personal information
information to third parties. However, we may share your information to third parties. However, we may share your information with:
with:
</p> </p>
<ul> <ul>
<li> <li>
@ -151,8 +150,8 @@ function privacyPolicy() {
</li> </li>
<li> <li>
<p> <p>
- Right to erasure: You can request the deletion of your - Right to erasure: You can request the deletion of your personal
personal data. data.
</p> </p>
</li> </li>
</ul> </ul>
@ -160,8 +159,8 @@ function privacyPolicy() {
<section> <section>
<h2>9. Changes to this Privacy Policy</h2> <h2>9. Changes to this Privacy Policy</h2>
<p> <p>
We may update this Privacy Policy to reflect changes in our We may update this Privacy Policy to reflect changes in our practices.
practices. The updated version will be posted on The updated version will be posted on
https://liljudd.ink/privacy-policy. https://liljudd.ink/privacy-policy.
</p> </p>
</section> </section>
@ -172,8 +171,7 @@ function privacyPolicy() {
please contact us at contact@moonleay.net. please contact us at contact@moonleay.net.
</p> </p>
</section> </section>
</div> </Layout>
</>
); );
} }

View file

@ -1,10 +1,11 @@
import Layout from "~/components/Layout";
import "../styles/pages/stack.scss"; import "../styles/pages/stack.scss";
function stack() { function stack() {
return ( return (
<> <Layout site="stack">
<h1 class="stack-title">The Stack</h1> <h1>The Stack</h1>
<section class="stack-section"> <section>
<img src="/assets/logos/kotlin.svg" alt="Kotlin 'K' logo" /> <img src="/assets/logos/kotlin.svg" alt="Kotlin 'K' logo" />
<div class="stackgrid_3 stackitm"> <div class="stackgrid_3 stackitm">
<h1>The Kotlin programming language</h1> <h1>The Kotlin programming language</h1>
@ -14,14 +15,14 @@ function stack() {
</p> </p>
</div> </div>
</section> </section>
<section class="stack-section"> <section>
<img src="/assets/logos/kord.png" alt="The Kord logo" /> <img src="/assets/logos/kord.png" alt="The Kord logo" />
<div class="stackgrid_3 stackitm"> <div class="stackgrid_3 stackitm">
<h1>The Kord library</h1> <h1>The Kord library</h1>
<p>A Kotlin library for creating Discord bots. Pretty bare bones.</p> <p>A Kotlin library for creating Discord bots. Pretty bare bones.</p>
</div> </div>
</section> </section>
<section class="stack-section"> <section>
<img <img
src="/assets/logos/kordextensions.png" src="/assets/logos/kordextensions.png"
alt="The Kord-Extensions logo" alt="The Kord-Extensions logo"
@ -31,7 +32,7 @@ function stack() {
<p>A Kotlin library to improve the Kord experience.</p> <p>A Kotlin library to improve the Kord experience.</p>
</div> </div>
</section> </section>
<section class="stack-section"> <section>
<img src="/assets/logos/pgelephant.png" alt="The PostgreSQL elephant" /> <img src="/assets/logos/pgelephant.png" alt="The PostgreSQL elephant" />
<div class="stackgrid_3 stackitm"> <div class="stackgrid_3 stackitm">
<h1>The PostgreSQL database</h1> <h1>The PostgreSQL database</h1>
@ -47,7 +48,7 @@ function stack() {
<a href="/acknowledgements">Acknowledgements</a>. <a href="/acknowledgements">Acknowledgements</a>.
</p> </p>
</section> </section>
</> </Layout>
); );
} }

View file

@ -1,22 +1,22 @@
import Layout from "~/components/Layout";
import "../styles/pages/terms-of-service.scss"; import "../styles/pages/terms-of-service.scss";
function termsOfService() { function termsOfService() {
return ( return (
<> <Layout site="termsOfService">
<div class="termsOfService">
<h1>Terms of Service</h1> <h1>Terms of Service</h1>
<div> <div>
<h2>Usage Agreement</h2> <h2>Usage Agreement</h2>
<p> <p>
By inviting the bot and using its features (commands, planning By inviting the bot and using its features (commands, planning system)
system) are you agreeing to the below mentioned Terms and Privacy are you agreeing to the below mentioned Terms and Privacy Policy
Policy (Policy) of the bot. (Policy) of the bot.
<br /> <br />
You acknowledge that you have the privilege to use the bot freely on You acknowledge that you have the privilege to use the bot freely on
any Discord Server (Server) you share with it, that you can invite any Discord Server (Server) you share with it, that you can invite it
it to any Server that you have "Manage Server" rights for and that to any Server that you have "Manage Server" rights for and that this
this privilege might get revoked for you, if you're subject of privilege might get revoked for you, if you're subject of breaking the
breaking the terms and/or policy of this bot, or the{" "} terms and/or policy of this bot, or the{" "}
<a href="https://discord.com/terms" target="_blank"> <a href="https://discord.com/terms" target="_blank">
Terms of Service Terms of Service
</a> </a>
@ -33,11 +33,11 @@ function termsOfService() {
Discord Inc Discord Inc
</a> </a>
.<br /> .<br />
Through Inviting the bot may it collect specific data as described Through Inviting the bot may it collect specific data as described in
in its Policy. its Policy.
<br /> <br />
The intended usage of this data is for core functionalities of the The intended usage of this data is for core functionalities of the bot
bot such as command handling, guild-specific settings and the such as command handling, guild-specific settings and the
time-planning system. time-planning system.
<br /> <br />
</p> </p>
@ -45,11 +45,11 @@ function termsOfService() {
<div> <div>
<h2>Intended Age</h2> <h2>Intended Age</h2>
<p> <p>
The bot may not be used by individuals under the minimal age The bot may not be used by individuals under the minimal age described
described in Discord's Terms of Service. in Discord's Terms of Service.
<br /> <br />
Doing so will be seen as a violation of these terms and will result Doing so will be seen as a violation of these terms and will result in
in a removal of the bot from any servers you own. a removal of the bot from any servers you own.
<br /> <br />
</p> </p>
</div> </div>
@ -60,8 +60,7 @@ function termsOfService() {
<br /> <br />
Any direct connection to Discord or any of its Trademark objects is Any direct connection to Discord or any of its Trademark objects is
purely coincidental. We do not claim to have the copyright ownership purely coincidental. We do not claim to have the copyright ownership
of any of Discord's assets, trademarks or other intellectual of any of Discord's assets, trademarks or other intellectual property.
property.
<br /> <br />
</p> </p>
</div> </div>
@ -71,26 +70,25 @@ function termsOfService() {
The owner of the bot may not be made liable for individuals breaking The owner of the bot may not be made liable for individuals breaking
these Terms at any given time. these Terms at any given time.
<br /> <br />
He has faith in the end users being truthfull about their He has faith in the end users being truthfull about their information
information and not misusing this bot or the services of Discord Inc and not misusing this bot or the services of Discord Inc in a
in a malicious way. malicious way.
<br /> <br />
We reserve the right to update these terms at our own discretion, We reserve the right to update these terms at our own discretion,
giving you a 1-Week (7 days) period to opt out of these terms if giving you a 1-Week (7 days) period to opt out of these terms if
you're not agreeing with the new changes. You may opt out by you're not agreeing with the new changes. You may opt out by Removing
Removing the bot from any Server you have the rights for. the bot from any Server you have the rights for.
</p> </p>
</div> </div>
<div> <div>
<h2>Contact</h2> <h2>Contact</h2>
<p> <p>
People may get in contact through e-mail at contact@moonleay.net, or People may get in contact through e-mail at contact@moonleay.net, or
through the official Support Discord of the Bot. Other ways of through the official Support Discord of the Bot. Other ways of support
support may be provided but aren't guaranteed. may be provided but aren't guaranteed.
</p> </p>
</div> </div>
</div> </Layout>
</>
); );
} }

View file

@ -5,46 +5,73 @@ nav {
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
position: sticky; position: sticky;
border-radius: 5px; border-radius: 5px;
padding: 10px;
ul { ul .navElem a {
display: flex;
flex-direction: column;
align-items: center;
padding: 0;
.navElem {
margin: 0.5rem 1rem;
transition: 0.5s;
color: white; color: white;
padding: 0 1rem;
vertical-align: middle;
&:hover { &:hover {
color: rgb(96 59 255) !important; color: rgb(96 59 255) !important;
.swap {
svg path {
transition: color 0.5s 0.5s;
} }
.textBx { .primary {
display: flex; opacity: 0;
align-items: center; }
#logo { .secondary {
opacity: 1;
}
}
}
img,
.swap,
.swap svg {
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 100%; border-radius: 100%;
max-width: initial; max-width: initial;
max-height: initial; max-height: initial;
margin: 0.5rem; }
.swap {
position: relative;
> * {
transition: opacity 0.5s;
position: absolute;
}
img {
margin: 0;
}
.secondary {
opacity: 0;
} }
} }
span,
> svg path {
transition: color 0.5s;
}
.lower {
margin-top: 2px;
} }
} }
@media (min-width: 768px) { @media (min-width: 768px) {
ul { justify-content: space-between;
flex-direction: row;
padding: 0 2rem;
.navElem:last-child { ul {
margin-left: auto; height: 72px;
}
} }
} }
} }

View file

@ -53,3 +53,24 @@ a {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
.flex-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
&.centered {
justify-content: center;
}
&.thick {
align-items: initial;
}
&.responsive {
@media (max-width: 768px) {
flex-direction: column;
}
}
}

View file

@ -1,4 +1,4 @@
.aboutdiv { .about {
max-width: 1100px; max-width: 1100px;
margin: 1rem auto; margin: 1rem auto;
width: 100%; width: 100%;

View file

@ -1,38 +1,34 @@
.guildpfp { .config {
.text-center {
text-align: center;
}
.guildpfp {
width: 50px; width: 50px;
height: 50px; height: 50px;
border-radius: 100%; border-radius: 100%;
}
.horizontal {
display: flex;
flex-direction: row;
h1 {
margin-left: 10px;
} }
p, h3, h4 {
label {
margin-right: 10px; margin-right: 10px;
} }
}
.marg_right_5px { section,
margin-right: 10px; a {
}
.centered {
text-align: center;
justify-content: center;
align-items: center;
}
.config {
article {
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
border-radius: 4px; border-radius: 4px;
padding: 1rem; padding: 1rem;
max-width: 1100px; max-width: 1100px;
margin: 1rem auto; margin: 1rem auto;
width: 100%; width: 100%;
}
.sub {
margin-left: 10px;
&.disabled {
pointer-events: none;
opacity: 0.5;
}
} }
} }

View file

@ -8,7 +8,7 @@
} }
text-align: center; text-align: center;
.contact { section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View file

@ -3,7 +3,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
.title { h1 {
font-size: 3rem; font-size: 3rem;
text-align: center; text-align: center;
margin-bottom: 1.2rem; margin-bottom: 1.2rem;

View file

@ -1,10 +1,11 @@
.hdi-title { .how-do-i {
h1 {
font-size: 3rem; font-size: 3rem;
text-align: center; text-align: center;
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
} }
.hdi-section { section {
max-width: 1100px; max-width: 1100px;
margin: 1rem auto; margin: 1rem auto;
width: 100%; width: 100%;
@ -39,9 +40,9 @@
} }
} }
} }
} }
.hdi-footernotesection { .note {
max-width: 1100px; max-width: 1100px;
margin: 1rem auto; margin: 1rem auto;
width: 100%; width: 100%;
@ -61,4 +62,5 @@
color: rgb(96 59 255) !important; color: rgb(96 59 255) !important;
} }
} }
}
} }

View file

@ -1,10 +1,11 @@
.stack-title { .stack {
h1 {
font-size: 3rem; font-size: 3rem;
text-align: center; text-align: center;
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
} }
.stack-section { section {
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
border-radius: 4px; border-radius: 4px;
margin: 1rem; margin: 1rem;
@ -33,9 +34,9 @@
align-self: center; align-self: center;
margin: auto; margin: auto;
} }
} }
.stack-note { .note {
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
border-radius: 4px; border-radius: 4px;
margin: 1rem; margin: 1rem;
@ -58,4 +59,5 @@
color: rgb(96 59 255) !important; color: rgb(96 59 255) !important;
} }
} }
}
} }