feat: render dashboard fully on client

This commit is contained in:
Jozef Steinhübl 2024-08-03 14:42:34 +02:00
parent 6ccd0c4d3b
commit 0259cb1e74
No known key found for this signature in database
GPG key ID: E6BC90C91973B08F
14 changed files with 153 additions and 114 deletions

View file

@ -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: {

View file

@ -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.

View file

@ -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": {

View 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>

View 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>
);
}

View 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} />);
}
}

View 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>
);
}

View file

@ -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;
}

View file

@ -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);

View 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;
}

View file

@ -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,
}))
);
}; };

View file

@ -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,
});
};

View file

@ -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>