mirror of
https://github.com/xHyroM/roles-bot.git
synced 2024-11-22 16:31:05 +01:00
feat: render dashboard fully on client
This commit is contained in:
parent
6ccd0c4d3b
commit
0259cb1e74
14 changed files with 153 additions and 114 deletions
|
@ -1,10 +1,12 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { defineConfig } from "astro/config";
|
import { defineConfig } from "astro/config";
|
||||||
|
import preact from "@astrojs/preact";
|
||||||
import sitemap from "@astrojs/sitemap";
|
import sitemap from "@astrojs/sitemap";
|
||||||
import tailwind from "@astrojs/tailwind";
|
import tailwind from "@astrojs/tailwind";
|
||||||
import cloudflare from "@astrojs/cloudflare";
|
import cloudflare from "@astrojs/cloudflare";
|
||||||
import auth from "auth-astro";
|
import auth from "auth-astro";
|
||||||
|
|
||||||
import { CONFIG } from "./src/config";
|
import { CONFIG } from "./src/config";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
@ -12,7 +14,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: CONFIG.origin,
|
site: CONFIG.origin,
|
||||||
integrations: [sitemap(), tailwind(), auth()],
|
integrations: [sitemap(), tailwind(), auth(), preact()],
|
||||||
output: "server",
|
output: "server",
|
||||||
adapter: cloudflare(),
|
adapter: cloudflare(),
|
||||||
vite: {
|
vite: {
|
||||||
|
|
|
@ -29,17 +29,8 @@ export default defineConfig({
|
||||||
(session.user as unknown as User).global_name =
|
(session.user as unknown as User).global_name =
|
||||||
token.global_name as string;
|
token.global_name as string;
|
||||||
|
|
||||||
const guilds = await fetch(
|
(session.user as unknown as User).discordAccessToken =
|
||||||
"https://discord.com/api/v10/users/@me/guilds",
|
token.accessToken as string;
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token.accessToken as string}`,
|
|
||||||
"Cache-Control": "max-age=300",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
(session.user as unknown as User).guilds = await guilds.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
|
|
Binary file not shown.
|
@ -10,6 +10,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "^11.0.1",
|
"@astrojs/cloudflare": "^11.0.1",
|
||||||
|
"@astrojs/preact": "^3.5.1",
|
||||||
"@astrojs/prefetch": "^0.2.1",
|
"@astrojs/prefetch": "^0.2.1",
|
||||||
"@astrojs/sitemap": "^3.1.6",
|
"@astrojs/sitemap": "^3.1.6",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
"astro-google-fonts-optimizer": "^0.2.2",
|
"astro-google-fonts-optimizer": "^0.2.2",
|
||||||
"astro-icon": "^0.8.0",
|
"astro-icon": "^0.8.0",
|
||||||
"auth-astro": "^4.1.2",
|
"auth-astro": "^4.1.2",
|
||||||
|
"preact": "^10.23.1",
|
||||||
"tailwindcss": "^3.3.1"
|
"tailwindcss": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
23
apps/website/src/components/dashboard/UserInfo.astro
Normal file
23
apps/website/src/components/dashboard/UserInfo.astro
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import { getUser } from "~/lib/user";
|
||||||
|
|
||||||
|
const user = await getUser(Astro.request);
|
||||||
|
if (!user) {
|
||||||
|
return Astro.redirect("/auth/login");
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<Image
|
||||||
|
src={user.image!}
|
||||||
|
class="mr-5 rounded-full"
|
||||||
|
alt="icon"
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h1 class="break-all py-12 text-5xl font-extrabold text-white">
|
||||||
|
@{user.name}
|
||||||
|
</h1>
|
||||||
|
</div>
|
28
apps/website/src/components/preact/Guild.tsx
Normal file
28
apps/website/src/components/preact/Guild.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import type { Guild } from "~/env";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
guild: Guild;
|
||||||
|
mutual: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Guild({ guild, mutual }: Props) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
class={`flex min-h-max w-80 justify-between rounded-md border-[1px] border-neutral-800 bg-dark-100 p-6 md:w-96 ${
|
||||||
|
!mutual && "brightness-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<img
|
||||||
|
src={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp`}
|
||||||
|
class="mr-5 rounded-full"
|
||||||
|
alt="icon"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="w-fit break-all text-3xl font-bold">{guild.name}</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
31
apps/website/src/components/preact/GuildSelector.tsx
Normal file
31
apps/website/src/components/preact/GuildSelector.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Component } from "preact";
|
||||||
|
import Guild from "./Guild";
|
||||||
|
import LoadingGuild from "./LoadingGuild";
|
||||||
|
import type { MutualeGuild } from "~/env";
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
guilds: MutualeGuild[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class GuildSelector extends Component<{}, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
guilds: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const response = await fetch("/api/user/guilds");
|
||||||
|
const data = await response.json();
|
||||||
|
this.setState({ guilds: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { guilds } = this.state;
|
||||||
|
if (!guilds) return [...Array(15)].map(() => <LoadingGuild />);
|
||||||
|
|
||||||
|
return guilds.map((g) => <Guild guild={g.guild} mutual={g.mutual} />);
|
||||||
|
}
|
||||||
|
}
|
9
apps/website/src/components/preact/LoadingGuild.tsx
Normal file
9
apps/website/src/components/preact/LoadingGuild.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export default function LoadingGuild() {
|
||||||
|
return (
|
||||||
|
<section className="flex min-h-max w-80 items-center justify-center rounded-md border border-neutral-800 bg-dark-100 p-6 md:w-96">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="h-6 w-52 animate-pulse rounded-md bg-neutral-700 shadow-md"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
7
apps/website/src/env.d.ts
vendored
7
apps/website/src/env.d.ts
vendored
|
@ -4,7 +4,7 @@ import type { User as AuthCoreUser } from "@auth/core/types";
|
||||||
|
|
||||||
export type User = AuthCoreUser & {
|
export type User = AuthCoreUser & {
|
||||||
global_name: string;
|
global_name: string;
|
||||||
guilds: Guild[];
|
discordAccessToken: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Guild {
|
export interface Guild {
|
||||||
|
@ -14,3 +14,8 @@ export interface Guild {
|
||||||
permissions: string;
|
permissions: string;
|
||||||
owner: boolean;
|
owner: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MutualeGuild {
|
||||||
|
mutual: boolean;
|
||||||
|
guild: Guild;
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
import type { Guild } from "~/env";
|
import type { Guild, MutualeGuild, User } from "~/env";
|
||||||
|
|
||||||
|
export async function getUserGuilds(user: User): Promise<Guild[]> {
|
||||||
|
const discordApiGuildsResponse = await fetch(
|
||||||
|
"https://discord.com/api/v10/users/@me/guilds",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${user.discordAccessToken as string}`,
|
||||||
|
"Cache-Control": "max-age=300",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (await discordApiGuildsResponse.json()) as Guild[];
|
||||||
|
}
|
||||||
|
|
||||||
// Checks if user has AMDINISTRATOR permissions in the guild
|
// Checks if user has AMDINISTRATOR permissions in the guild
|
||||||
export async function filterUserGuilds(guilds: Guild[]): Promise<
|
export async function filterUserGuilds(
|
||||||
{
|
guilds: Guild[]
|
||||||
mutual: boolean;
|
): Promise<MutualeGuild[]> {
|
||||||
guild: Guild;
|
|
||||||
}[]
|
|
||||||
> {
|
|
||||||
const filtered = guilds
|
const filtered = guilds
|
||||||
.filter((g) => (BigInt(g.permissions) & 0x8n) == 0x8n)
|
.filter((g) => (BigInt(g.permissions) & 0x8n) == 0x8n)
|
||||||
.sort((a, b) => Number(b.owner) - Number(a.owner));
|
.sort((a, b) => Number(b.owner) - Number(a.owner));
|
||||||
|
|
||||||
const result: {
|
const result: MutualeGuild[] = [];
|
||||||
mutual: boolean;
|
|
||||||
guild: Guild;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
for (const guild of filtered) {
|
for (const guild of filtered) {
|
||||||
const mutual = await isMutualGuild(guild);
|
const mutual = await isMutualGuild(guild);
|
||||||
|
|
9
apps/website/src/lib/user.ts
Normal file
9
apps/website/src/lib/user.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { getSession } from "auth-astro/server";
|
||||||
|
import type { User } from "~/env";
|
||||||
|
|
||||||
|
export async function getUser(req: Request): Promise<User | null> {
|
||||||
|
const session = await getSession(req);
|
||||||
|
if (!session || !session.user) return null;
|
||||||
|
|
||||||
|
return session.user as User;
|
||||||
|
}
|
|
@ -1,23 +1,17 @@
|
||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { getSession } from "auth-astro/server";
|
import { filterUserGuilds, getUserGuilds } from "~/lib/guilds";
|
||||||
import type { User } from "~/env";
|
import { getUser } from "~/lib/user";
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
const session = await getSession(request);
|
const user = await getUser(request);
|
||||||
if (!session || !session.user) {
|
if (!user) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 401,
|
status: 401,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = session.user as User;
|
const guilds = await getUserGuilds(user);
|
||||||
|
const res = await filterUserGuilds(guilds);
|
||||||
|
|
||||||
return Response.json(
|
return Response.json(res);
|
||||||
user.guilds.map((g) => ({
|
|
||||||
id: g.id,
|
|
||||||
name: g.name,
|
|
||||||
owner: g.owner,
|
|
||||||
permissions: g.permissions,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import type { APIRoute } from "astro";
|
|
||||||
import { getSession } from "auth-astro/server";
|
|
||||||
import type { User } from "~/env";
|
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
|
||||||
const session = await getSession(request);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return new Response(null, {
|
|
||||||
status: 401,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = session.user as User;
|
|
||||||
|
|
||||||
return Response.json({
|
|
||||||
id: user.id,
|
|
||||||
username: user.name,
|
|
||||||
global_name: user.global_name,
|
|
||||||
avatar_url: user.image,
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,60 +1,18 @@
|
||||||
---
|
---
|
||||||
import type { User } from "~/env";
|
|
||||||
import { filterUserGuilds } from "~/lib/guilds";
|
|
||||||
import { getSession } from "auth-astro/server";
|
|
||||||
import Layout from "~/layouts/Layout.astro";
|
import Layout from "~/layouts/Layout.astro";
|
||||||
import Container from "~/components/Container.astro";
|
import Container from "~/components/Container.astro";
|
||||||
import { Image } from "astro:assets";
|
import GuildSelector from "~/components/preact/GuildSelector";
|
||||||
|
import UserInfo from "~/components/dashboard/UserInfo.astro";
|
||||||
const session = await getSession(Astro.request);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return Astro.redirect("/auth/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = session.user as User;
|
|
||||||
const guilds = await filterUserGuilds(user.guilds);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<div class="flex items-center justify-center">
|
<UserInfo />
|
||||||
<Image
|
|
||||||
src={user.image!}
|
|
||||||
class="mr-5 rounded-full"
|
|
||||||
alt="icon"
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h1 class="break-all py-12 text-5xl font-extrabold text-white">
|
|
||||||
@{user.name}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Container class="pb-4">
|
<Container class="pb-4">
|
||||||
<main
|
<main
|
||||||
class="flex flex-wrap items-center justify-center gap-12 pb-4 text-white"
|
class="flex flex-wrap items-center justify-center gap-12 pb-4 text-white"
|
||||||
>
|
>
|
||||||
{
|
<GuildSelector client:load />
|
||||||
guilds.map(({ guild, mutual }) => (
|
|
||||||
<section
|
|
||||||
class={`flex min-h-max w-80 justify-between rounded-md border-[1px] border-neutral-800 bg-dark-100 p-6 md:w-96 ${
|
|
||||||
!mutual && "brightness-50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div class="flex flex-row items-center">
|
|
||||||
<Image
|
|
||||||
src={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp`}
|
|
||||||
class="mr-5 rounded-full"
|
|
||||||
alt="icon"
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h2 class="w-fit break-all text-3xl font-bold">{guild.name}</h2>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</main>
|
</main>
|
||||||
</Container>
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
Loading…
Reference in a new issue