Compare commits
@@ -39,6 +39,10 @@ jobs:
|
||||
-t mts.dgnr.us:5000/${{ gitea.repository }}:frontend \
|
||||
-t mts.dgnr.us:5000/${{ gitea.repository }}:frontend-$TAG \
|
||||
.
|
||||
docker build --target migration \
|
||||
-t mts.dgnr.us:5000/${{ gitea.repository }}:migration \
|
||||
-t mts.dgnr.us:5000/${{ gitea.repository }}:migration-$TAG \
|
||||
.
|
||||
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -83,6 +87,8 @@ jobs:
|
||||
docker push mts.dgnr.us:5000/${{ gitea.repository }}:backend-$TAG &
|
||||
docker push mts.dgnr.us:5000/${{ gitea.repository }}:frontend &
|
||||
docker push mts.dgnr.us:5000/${{ gitea.repository }}:frontend-$TAG &
|
||||
docker push mts.dgnr.us:5000/${{ gitea.repository }}:migration &
|
||||
docker push mts.dgnr.us:5000/${{ gitea.repository }}:migration-$TAG &
|
||||
wait
|
||||
|
||||
- name: Install kubectl
|
||||
@@ -106,9 +112,12 @@ jobs:
|
||||
# DB migration
|
||||
kubectl delete job db-migrate -n buzz --ignore-not-found
|
||||
kubectl create job db-migrate \
|
||||
--image=mts.dgnr.us:5000/${{ gitea.repository }}:frontend-$TAG \
|
||||
--image=mts.dgnr.us:5000/${{ gitea.repository }}:migration-$TAG \
|
||||
-n buzz \
|
||||
-- bun dr push
|
||||
--dry-run=client -o json \
|
||||
-- bun dr push | \
|
||||
jq '.spec.template.spec.containers[0].envFrom = [{"secretRef": {"name": "buzz-env"}}]' | \
|
||||
kubectl apply -f -
|
||||
|
||||
# Roll app & backend
|
||||
kubectl set image deployment/app app=mts.dgnr.us:5000/${{ gitea.repository }}:frontend-$TAG -n buzz
|
||||
|
||||
@@ -4,17 +4,6 @@
|
||||
FROM oven/bun:canary-alpine AS deps
|
||||
WORKDIR /home/container
|
||||
|
||||
# RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
# --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
# apt-get update \
|
||||
# && apt-get install -y \
|
||||
# python3 \
|
||||
# make \
|
||||
# g++ \
|
||||
# sqlite3 \
|
||||
# libsqlite3-dev \
|
||||
# && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package*.json bun.lock ./
|
||||
COPY patches ./patches
|
||||
|
||||
@@ -50,7 +39,22 @@ RUN --mount=type=cache,target=/home/container/.next/cache,uid=1001,gid=1001 \
|
||||
# Fix nextjs caching(bind mount removes it after build process)
|
||||
RUN mkdir -p .next/cache
|
||||
|
||||
### Stage 3: runner ###
|
||||
### Stage 3: migration ###
|
||||
FROM oven/bun:canary-alpine AS migration
|
||||
WORKDIR /home/container
|
||||
|
||||
COPY --from=deps /home/container/node_modules ./node_modules
|
||||
COPY --from=deps /home/container/package*.json ./
|
||||
COPY --from=deps /home/container/bun.lock ./
|
||||
COPY --from=builder /home/container/drizzle.config.ts ./drizzle.config.ts
|
||||
COPY --from=builder /home/container/lib ./lib
|
||||
|
||||
COPY drizzle.config.ts ./
|
||||
COPY lib/db/ ./lib/db/
|
||||
|
||||
CMD ["bun", "drizzle-kit"]
|
||||
|
||||
### Stage 4: runner ###
|
||||
FROM oven/bun:canary-alpine AS runner
|
||||
|
||||
# Isolation
|
||||
@@ -58,19 +62,10 @@ RUN adduser -Du 1001 container
|
||||
USER container
|
||||
WORKDIR /home/container
|
||||
|
||||
COPY --from=builder /home/container/node_modules ./node_modules
|
||||
COPY --from=builder /home/container/next.config.ts ./
|
||||
COPY --from=builder /home/container/bun.lock ./
|
||||
COPY --from=builder /home/container/package*.json ./
|
||||
|
||||
COPY --from=builder /home/container/.next/standalone ./
|
||||
COPY --from=builder /home/container/public ./public
|
||||
COPY --from=builder /home/container/.env ./.env
|
||||
COPY --from=builder /home/container/.next ./.next
|
||||
|
||||
COPY --from=builder /home/container/util ./util
|
||||
COPY --from=builder /home/container/lib ./lib
|
||||
COPY --from=builder /home/container/tsconfig.json ./tsconfig.json
|
||||
COPY --from=builder /home/container/.next/static ./.next/static
|
||||
COPY --from=builder /home/container/.version ./.version
|
||||
COPY --from=builder /home/container/drizzle.config.ts ./drizzle.config.ts
|
||||
|
||||
CMD ["bun", "start"]
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
CMD ["bun", "server.js"]
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Background0 from "#/bg.webp";
|
||||
import Background1 from "#/bgassets/buzzbg1-r2.webp";
|
||||
import Background2 from "#/bgassets/buzzbg2-r1.webp";
|
||||
import Background3 from "#/bgassets/buzzbg3-r1.webp";
|
||||
import Background4 from "#/bgassets/buzzbg4-r1.webp";
|
||||
import Background5 from "#/bgassets/buzzbg5-r1.webp";
|
||||
import Background6 from "#/bgassets/buzzbg6-r1.webp";
|
||||
import Background7 from "#/bgassets/buzzbg7-r1.webp";
|
||||
import type { StaticImport } from "next/dist/shared/lib/get-img-props";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { StarsRenderer } from "@/components/stars";
|
||||
|
||||
type PrefixBackground = [`/${string}`, string | StaticImport, `/${string}`];
|
||||
const backgrounds = [
|
||||
// path prefix, background, stars mask
|
||||
["/artifact", Background1, "/bgassets/buzzbg1-r2-mask.webp"],
|
||||
["/rubgram", Background2, "/bgassets/buzzbg2-r1-mask.webp"],
|
||||
["/donate", Background3, "/bgassets/buzzbg3-r1-mask.webp"],
|
||||
["/tl", Background5, "/bgassets/buzzbg4-r1-mask.webp"],
|
||||
["/guide", Background4, "/bgassets/buzzbg5-r1-mask.webp"],
|
||||
["/admin", Background6, "/bgassets/buzzbg6-r1-mask.webp"],
|
||||
// unused
|
||||
["/donate", Background7, "/bgassets/buzzbg7-r1-mask.webp"],
|
||||
// this should be last
|
||||
["/", Background0, "/mask.webp"],
|
||||
] satisfies PrefixBackground[];
|
||||
|
||||
export function BackgroundProvider() {
|
||||
const pathname = usePathname();
|
||||
const [, background, mask] = useMemo(
|
||||
() => backgrounds.find((b) => pathname.startsWith(b[0]))!,
|
||||
[pathname],
|
||||
);
|
||||
const [transitioning, setTransitioning] = useState(false);
|
||||
const [current, setCurrent] = useState<string | StaticImport>(background);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src={background}
|
||||
alt="Background Swap"
|
||||
className={cn(
|
||||
"fixed top-0 left-0 z-[-1] h-fit min-h-dvh w-full object-cover opacity-0",
|
||||
transitioning && "opacity-40 transition-opacity duration-500",
|
||||
)}
|
||||
onLoad={() => {
|
||||
if (background === current) return;
|
||||
setTransitioning(true);
|
||||
}}
|
||||
onTransitionEnd={() => {
|
||||
if (!transitioning) return;
|
||||
setCurrent(background);
|
||||
setTransitioning(false);
|
||||
}}
|
||||
/>
|
||||
<Image
|
||||
src={current}
|
||||
alt="Background Current"
|
||||
className={cn(
|
||||
"fixed top-0 left-0 z-[-1] h-fit min-h-dvh w-full object-cover opacity-40",
|
||||
transitioning && "opacity-0 transition-opacity duration-500",
|
||||
)}
|
||||
/>
|
||||
{transitioning || <StarsRenderer mask={mask} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useEllipsisVisible } from "react-hook-text-overflow";
|
||||
import { DataTable } from "@/components/tantable";
|
||||
import { SimpleTooltip } from "@/components/tooltip";
|
||||
@@ -183,23 +183,15 @@ export function DonateAdminPage({
|
||||
}
|
||||
|
||||
// component cuz simplicity sake
|
||||
export function DonateWatcher({ sfx = false }: { sfx?: boolean }) {
|
||||
const player = useRef<HTMLAudioElement | null>(null);
|
||||
export function DonateWatcher() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (!router) return;
|
||||
return sse.donate.subMany({
|
||||
ping: () => {
|
||||
router.refresh();
|
||||
player.current?.play();
|
||||
},
|
||||
update: () => router.refresh(),
|
||||
ping: router.refresh.bind(router),
|
||||
update: router.refresh.bind(router),
|
||||
}).clean;
|
||||
}, [router]);
|
||||
|
||||
if (sfx)
|
||||
return (
|
||||
<audio src="/assets/donate-mod-sfx.wav" preload="auto" ref={player} />
|
||||
);
|
||||
else return undefined;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { shared } from "@/lib/comms";
|
||||
import { sse } from "@/lib/db/sse-endpoints";
|
||||
import { ExternalLink, RadioTower } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -39,22 +41,55 @@ export function PIP(props: ComponentProps<typeof ReactPIP>) {
|
||||
|
||||
export function ClientTracker() {
|
||||
const [connected, setConnected] = useState(0);
|
||||
const player = useRef<HTMLAudioElement>(null);
|
||||
const tracking = useRef(0);
|
||||
|
||||
shared.signal(
|
||||
"moderator.volume",
|
||||
(ev) => player.current && (player.current.volume = ev ?? 1),
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => sse.donate.sub("heartbeat", () => tracking.current++).clean,
|
||||
() =>
|
||||
player.current
|
||||
? sse.donate.subMany({
|
||||
heartbeat: () => tracking.current++,
|
||||
ping: player.current.play.bind(player.current),
|
||||
}).clean
|
||||
: undefined,
|
||||
[],
|
||||
);
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setConnected(tracking.current);
|
||||
if (tracking.current > 0) tracking.current--;
|
||||
tracking.current = 0;
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return (
|
||||
<Button variant="outline" disabled size="sm">
|
||||
<RadioTower /> Connected: {connected}
|
||||
<RadioTower /> {connected}
|
||||
<audio
|
||||
src="/assets/donate-mod-sfx.wav"
|
||||
preload="auto"
|
||||
ref={player}
|
||||
hidden
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function VolumeChanger() {
|
||||
const [volume, setVolume] = shared.state("moderator.volume");
|
||||
|
||||
return (
|
||||
<div className="px-5 py-1">
|
||||
<Slider
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[volume ?? 0.5]}
|
||||
onValueChange={(v) => setVolume(v[0])}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Spinner } from "@/components/ui/spinner";
|
||||
import { ActionButton } from "@/components/action-button";
|
||||
import { reloadWidget } from "../api";
|
||||
import { markDone } from "@/app/widget/donate/api";
|
||||
import { ClientTracker, PIP } from "./client";
|
||||
import { ClientTracker, PIP, VolumeChanger } from "./client";
|
||||
|
||||
export default async function DonateModeratorPage() {
|
||||
if (!(await adminCheck()))
|
||||
@@ -32,7 +32,7 @@ export default async function DonateModeratorPage() {
|
||||
|
||||
return (
|
||||
<PIP width={400} height={600}>
|
||||
<div className="dark flex w-full max-w-100 flex-col gap-1 bg-background p-1 text-foreground scheme-dark">
|
||||
<div className="dark flex w-full max-w-100 flex-col gap-1 rounded-md bg-background p-1 text-foreground scheme-dark">
|
||||
<span>Donate Moderator</span>
|
||||
{sub.image ? (
|
||||
<div className="relative flex aspect-square w-full max-w-100 items-center justify-center overflow-hidden rounded-lg border bg-black/50">
|
||||
@@ -67,7 +67,8 @@ export default async function DonateModeratorPage() {
|
||||
</ActionButton>
|
||||
<ClientTracker />
|
||||
</div>
|
||||
<DonateWatcher sfx />
|
||||
<VolumeChanger />
|
||||
<DonateWatcher />
|
||||
</div>
|
||||
</PIP>
|
||||
);
|
||||
|
||||
@@ -191,13 +191,20 @@ export default async function () {
|
||||
|
||||
const { name, amount, message, image } = $;
|
||||
|
||||
const downscaled = image
|
||||
? await new Bun.Image(await image.arrayBuffer())
|
||||
.resize(512, 512)
|
||||
.webp()
|
||||
.toBuffer()
|
||||
: undefined;
|
||||
|
||||
const [{ id }] = await tx
|
||||
.insert(donations)
|
||||
.values({
|
||||
name,
|
||||
amount,
|
||||
message,
|
||||
image: image ? Buffer.from(await image.arrayBuffer()) : undefined,
|
||||
image: downscaled,
|
||||
uid: $.artifact === "true" ? $.uid : null,
|
||||
// dont send on screen if less than 10
|
||||
sent: $.amount < 10,
|
||||
@@ -290,7 +297,7 @@ export default async function () {
|
||||
</FormInput>
|
||||
</div>
|
||||
</div>
|
||||
<FormInput name="message" label="ข้อความ" subLabel="สูงสุด 500 ตัวอักษร">
|
||||
<FormInput name="message" label="ข้อความ" subLabel="สูงสุด 200 ตัวอักษร">
|
||||
<Textarea placeholder="ข้อความ" />
|
||||
</FormInput>
|
||||
{!artifactConfig.locked && (
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Anuphan } from "next/font/google";
|
||||
import { ViewTransition } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import Background from "#/bg.webp";
|
||||
import Image from "@/components/image";
|
||||
import { StarsRenderer } from "@/components/stars";
|
||||
import Providers, { VersionCheck } from "../client";
|
||||
import { Navbar } from "./navbar";
|
||||
import { BackgroundProvider } from "./background";
|
||||
|
||||
const anuphan = Anuphan({
|
||||
variable: "--font-anuphan",
|
||||
@@ -19,12 +17,7 @@ export default async function UiLayout({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src={Background}
|
||||
alt="Background"
|
||||
className="fixed top-0 left-0 z-[-1] h-fit min-h-dvh w-full object-cover opacity-40"
|
||||
/>
|
||||
<StarsRenderer />
|
||||
<BackgroundProvider />
|
||||
<Providers>
|
||||
<ViewTransition>{children}</ViewTransition>
|
||||
<VersionCheck />
|
||||
|
||||
@@ -126,7 +126,10 @@ export function SlipUpload({
|
||||
type="button"
|
||||
>
|
||||
{isSelected ? (
|
||||
<span onClick={isSelected ? choose : undefined} className="truncate">
|
||||
<span
|
||||
onClick={isSelected ? choose : undefined}
|
||||
className="min-w-0 truncate"
|
||||
>
|
||||
{value.name} <Kbd>{b2sClient(Number(value.size))}</Kbd>
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -129,7 +129,10 @@ export default async function AdminLayout({
|
||||
<SidebarGroupLabel>การตั้งค่า</SidebarGroupLabel>
|
||||
<LimitManager
|
||||
config={config}
|
||||
length={subs.reduce((c, s) => c + (s.paid && !s.deleted ? 1 : 0), 0)}
|
||||
length={subs.reduce(
|
||||
(c, s) => c + (s.paid && !s.deleted ? 1 : 0),
|
||||
0,
|
||||
)}
|
||||
/>
|
||||
</SidebarGroup>
|
||||
</SidebarFooter>
|
||||
|
||||
@@ -10,6 +10,7 @@ export default function TopDonateWidget() {
|
||||
const [top, setTop] = useState<Data>();
|
||||
const textRef = useRef<HTMLDivElement | null>(null);
|
||||
const stableSize = useRef<Record<string, number>>({});
|
||||
const [ping, setPing] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
async function update() {
|
||||
@@ -30,6 +31,12 @@ export default function TopDonateWidget() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => setPing(Date.now());
|
||||
window.addEventListener("resize", listener);
|
||||
return () => window.removeEventListener("resize", listener);
|
||||
});
|
||||
|
||||
// the most idiomatic way possible but it works
|
||||
useEffect(() => {
|
||||
if (!textRef.current) return;
|
||||
@@ -52,9 +59,9 @@ export default function TopDonateWidget() {
|
||||
}
|
||||
}, 100);
|
||||
return () => clearInterval(interval);
|
||||
}, [textRef, top]);
|
||||
}, [textRef, top, ping]);
|
||||
return top ? (
|
||||
<div className="grid h-18.75 w-94.5 max-w-94.5 grid-cols-[40px_minmax(0,1fr)_max-content] gap-1 rounded-full border-4 border-black bg-black/75 p-2 px-3 text-5xl font-semibold text-[#FFBA00] *:self-center">
|
||||
<div className="grid h-full w-full grid-cols-[40px_minmax(0,1fr)_max-content] gap-1 rounded-full border-4 border-black bg-black/75 p-2 px-3 text-5xl font-semibold text-[#FFBA00] *:self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24px"
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"@sfjs/snowflake": "^0.1.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.14.2",
|
||||
"@types/bun": "^1.3.12",
|
||||
"@types/bun": "^1.3.14",
|
||||
"auto-text-size": "^0.2.3",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"better-auth": "^1.4.6",
|
||||
@@ -64,7 +64,6 @@
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.6",
|
||||
"react-day-picker": "^9.12.0",
|
||||
"react-document-picture-in-picture": "^1.2.2",
|
||||
"react-dom": "19.2.6",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-hook-text-overflow": "^1.0.1",
|
||||
@@ -111,7 +110,6 @@
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"[email protected]": "patches/[email protected]",
|
||||
"[email protected]": "patches/[email protected]",
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "19.2.14",
|
||||
@@ -729,7 +727,7 @@
|
||||
|
||||
"@tanstack/virtual-core": ["@tanstack/[email protected]", "", {}, "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ=="],
|
||||
|
||||
"@types/bun": ["@types/[email protected]2", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
||||
"@types/bun": ["@types/[email protected]4", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/d3-array": ["@types/[email protected]", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
@@ -803,7 +801,7 @@
|
||||
|
||||
"buffer-from": ["[email protected]", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["[email protected]2", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||
"bun-types": ["[email protected]4", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"bundle-name": ["[email protected]", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
@@ -1173,8 +1171,6 @@
|
||||
|
||||
"react-day-picker": ["[email protected]", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ=="],
|
||||
|
||||
"react-document-picture-in-picture": ["[email protected]", "", { "peerDependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" } }, "sha512-MqwBF4gL56qe8QoFGu/Cc2QxplZcCMK125gMqcGvQQm8xL3z1iiGPTUE2Fk7Ct6IEvmuYtsupsdIwCNl2i16aQ=="],
|
||||
|
||||
"react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
||||
|
||||
"react-hook-form": ["[email protected]", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="],
|
||||
|
||||
@@ -168,6 +168,13 @@ export const DocumentPictureInPicture = forwardRef<
|
||||
|
||||
div.style.width = "100%";
|
||||
div.style.height = "100%";
|
||||
div.style.background = "black";
|
||||
div.style.backgroundImage = "radial-gradient(#222 1px, transparent 1px)";
|
||||
div.style.backgroundSize = "24px 24px";
|
||||
div.style.height = "100svh";
|
||||
div.style.display = "flex";
|
||||
div.style.justifyContent = "center";
|
||||
div.style.alignItems = "center";
|
||||
|
||||
win.document.body.appendChild(div);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function StarsRenderer() {
|
||||
export function StarsRenderer({ mask }: { mask: string }) {
|
||||
const cv = useRef<HTMLCanvasElement | null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
@@ -122,9 +122,12 @@ export function StarsRenderer() {
|
||||
<canvas
|
||||
ref={cv}
|
||||
className={cn(
|
||||
"fixed top-0 left-0 z-[-1] h-fit min-h-dvh w-full mask-[url('/mask.webp')] mask-cover mask-center mask-no-repeat object-cover opacity-0 transition-opacity duration-5000",
|
||||
"fixed top-0 left-0 z-[-1] h-fit min-h-dvh w-full mask-cover mask-center mask-no-repeat object-cover opacity-0 transition-opacity duration-5000",
|
||||
loaded && "opacity-40",
|
||||
)}
|
||||
style={{
|
||||
maskImage: `url(${JSON.stringify(mask)})`,
|
||||
}}
|
||||
></canvas>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ type SharedStates = Partial<{
|
||||
warning: "showcase" | "nf" | "guide" | "private";
|
||||
"warning.uid": string;
|
||||
"warning.src": "submit" | "input";
|
||||
"moderator.volume": number;
|
||||
_raw: Record<string, unknown>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -52,4 +52,5 @@ export default {
|
||||
// reactCompiler: true,
|
||||
allowedDevOrigins: ["astral:3000", "dev3000.astrxl.dev"],
|
||||
// basePath: "/beta"
|
||||
output: "standalone",
|
||||
} satisfies NextConfig;
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"@sfjs/snowflake": "^0.1.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.14.2",
|
||||
"@types/bun": "^1.3.12",
|
||||
"@types/bun": "^1.3.14",
|
||||
"auto-text-size": "^0.2.3",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"better-auth": "^1.4.6",
|
||||
@@ -78,7 +78,6 @@
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.6",
|
||||
"react-day-picker": "^9.12.0",
|
||||
"react-document-picture-in-picture": "^1.2.2",
|
||||
"react-dom": "19.2.6",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-hook-text-overflow": "^1.0.1",
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 839 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 1016 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,14 @@
|
||||
import { file, Glob, write } from "bun";
|
||||
import { rm } from "node:fs/promises";
|
||||
|
||||
const g = new Glob("public/bgassets/*.png");
|
||||
|
||||
for await (const img of g.scan()) {
|
||||
file(img)
|
||||
.image()
|
||||
.webp()
|
||||
.buffer()
|
||||
.then((b) => write(img.replace(".png", ".webp"), b))
|
||||
.then(() => rm(img))
|
||||
.finally(() => console.log(img, "processed."));
|
||||
}
|
||||
@@ -97,10 +97,10 @@ async function main() {
|
||||
console.log("Fetching Amber data...");
|
||||
const [changelog, avatars]: [ChangelogApiResponse, AvatarApiResponse] =
|
||||
await Promise.all([
|
||||
await fetch("https://gi.yatta.moe/api/v2/static/changelog").then((e) =>
|
||||
await fetch("https://buzz.sudloh.com/api/amber/log").then((e) =>
|
||||
e.json(),
|
||||
),
|
||||
await fetch("https://gi.yatta.moe/api/v2/en/avatar").then((e) =>
|
||||
await fetch("https://buzz.sudloh.com/api/amber/char").then((e) =>
|
||||
e.json(),
|
||||
),
|
||||
]);
|
||||
|
||||