mirror of
https://github.com/xHyroM/roles-bot.git
synced 2024-11-22 08:21: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,25 +1,27 @@
|
|||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { defineConfig } from "astro/config";
|
||||
import preact from "@astrojs/preact";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import cloudflare from "@astrojs/cloudflare";
|
||||
import auth from "auth-astro";
|
||||
|
||||
import { CONFIG } from "./src/config";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: CONFIG.origin,
|
||||
integrations: [sitemap(), tailwind(), auth()],
|
||||
output: "server",
|
||||
adapter: cloudflare(),
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: {
|
||||
"~": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
},
|
||||
site: CONFIG.origin,
|
||||
integrations: [sitemap(), tailwind(), auth(), preact()],
|
||||
output: "server",
|
||||
adapter: cloudflare(),
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: {
|
||||
"~": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -29,17 +29,8 @@ export default defineConfig({
|
|||
(session.user as unknown as User).global_name =
|
||||
token.global_name as string;
|
||||
|
||||
const guilds = await fetch(
|
||||
"https://discord.com/api/v10/users/@me/guilds",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.accessToken as string}`,
|
||||
"Cache-Control": "max-age=300",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
(session.user as unknown as User).guilds = await guilds.json();
|
||||
(session.user as unknown as User).discordAccessToken =
|
||||
token.accessToken as string;
|
||||
}
|
||||
|
||||
return session;
|
||||
|
|
Binary file not shown.
|
@ -10,6 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "^11.0.1",
|
||||
"@astrojs/preact": "^3.5.1",
|
||||
"@astrojs/prefetch": "^0.2.1",
|
||||
"@astrojs/sitemap": "^3.1.6",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
|
@ -19,6 +20,7 @@
|
|||
"astro-google-fonts-optimizer": "^0.2.2",
|
||||
"astro-icon": "^0.8.0",
|
||||
"auth-astro": "^4.1.2",
|
||||
"preact": "^10.23.1",
|
||||
"tailwindcss": "^3.3.1"
|
||||
},
|
||||
"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 & {
|
||||
global_name: string;
|
||||
guilds: Guild[];
|
||||
discordAccessToken: string;
|
||||
};
|
||||
|
||||
export interface Guild {
|
||||
|
@ -14,3 +14,8 @@ export interface Guild {
|
|||
permissions: string;
|
||||
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
|
||||
export async function filterUserGuilds(guilds: Guild[]): Promise<
|
||||
{
|
||||
mutual: boolean;
|
||||
guild: Guild;
|
||||
}[]
|
||||
> {
|
||||
export async function filterUserGuilds(
|
||||
guilds: Guild[]
|
||||
): Promise<MutualeGuild[]> {
|
||||
const filtered = guilds
|
||||
.filter((g) => (BigInt(g.permissions) & 0x8n) == 0x8n)
|
||||
.sort((a, b) => Number(b.owner) - Number(a.owner));
|
||||
|
||||
const result: {
|
||||
mutual: boolean;
|
||||
guild: Guild;
|
||||
}[] = [];
|
||||
const result: MutualeGuild[] = [];
|
||||
|
||||
for (const guild of filtered) {
|
||||
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 { getSession } from "auth-astro/server";
|
||||
import type { User } from "~/env";
|
||||
import { filterUserGuilds, getUserGuilds } from "~/lib/guilds";
|
||||
import { getUser } from "~/lib/user";
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
const session = await getSession(request);
|
||||
if (!session || !session.user) {
|
||||
const user = await getUser(request);
|
||||
if (!user) {
|
||||
return new Response(null, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const user = session.user as User;
|
||||
const guilds = await getUserGuilds(user);
|
||||
const res = await filterUserGuilds(guilds);
|
||||
|
||||
return Response.json(
|
||||
user.guilds.map((g) => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
owner: g.owner,
|
||||
permissions: g.permissions,
|
||||
}))
|
||||
);
|
||||
return Response.json(res);
|
||||
};
|
||||
|
|
|
@ -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 Container from "~/components/Container.astro";
|
||||
import { Image } from "astro:assets";
|
||||
|
||||
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);
|
||||
import GuildSelector from "~/components/preact/GuildSelector";
|
||||
import UserInfo from "~/components/dashboard/UserInfo.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<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>
|
||||
<UserInfo />
|
||||
|
||||
<Container class="pb-4">
|
||||
<main
|
||||
class="flex flex-wrap items-center justify-center gap-12 pb-4 text-white"
|
||||
>
|
||||
{
|
||||
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>
|
||||
))
|
||||
}
|
||||
<GuildSelector client:load />
|
||||
</main>
|
||||
</Container>
|
||||
</Layout>
|
||||
|
|
Loading…
Reference in a new issue