feat(don): add top donor widget

This commit is contained in:
2026-06-10 01:36:54 +07:00 Unverified
parent 652b83c801
commit 85aaf6487c
28 changed files with 222 additions and 42 deletions
+9 -2
View File
@@ -1,10 +1,17 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["nextjs"],
"plugins": ["nextjs", "react", "import", "typescript"],
"rules": {
"nextjs/no-html-link-for-pages": "error",
"nextjs/next-script-for-ga": "error",
"typescript/no-import-type-side-effects": "error",
"typescript/consistent-type-imports": "error"
"typescript/consistent-type-imports": "error",
"typescript/restrict-template-expressions": "off",
"typescript/no-floating-promises": "off",
"typescript/no-base-to-string": "off",
"typescript/unbound-method": "off"
},
"options": {
"typeAware": true
}
}
+2 -1
View File
@@ -21,9 +21,10 @@ import { Spinner } from "@/components/ui/spinner";
import { actionLog } from "@/lib/api";
import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db";
import type {
element} from "@/lib/db/schema";
import {
characters,
element,
element as elementEnum,
versions,
} from "@/lib/db/schema";
+2 -1
View File
@@ -21,9 +21,10 @@ import { Spinner } from "@/components/ui/spinner";
import { actionLog } from "@/lib/api";
import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db";
import type {
element} from "@/lib/db/schema";
import {
characters,
element,
element as elementEnum,
versions,
} from "@/lib/db/schema";
+1 -1
View File
@@ -93,7 +93,7 @@ export function RandomWelcomeMessage() {
useEffect(() => {
setIndex(Math.floor(Math.random() * items.length));
}, []);
}, [items.length]);
if (index === -1)
return (
+2 -1
View File
@@ -35,6 +35,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { getArtifactConfig } from "@/lib/api";
import { setLimit, toggleCheck, toggleLock } from "@/lib/api";
import { shared } from "@/lib/comms";
import { sse } from "@/lib/db/sse-endpoints";
@@ -139,7 +140,7 @@ export function LimitManager({
length,
...props
}: React.ComponentProps<typeof SidebarMenu> & {
config: Awaited<ReturnType<typeof import("@/lib/api").getArtifactConfig>>;
config: Awaited<ReturnType<typeof getArtifactConfig>>;
length: number;
}) {
return (
+13 -3
View File
@@ -11,7 +11,10 @@ import { getPostHogClient } from "@/lib/posthog-server";
export async function testPopup() {
if (!(await adminCheck())) throw new Error("Unauthorized");
getPostHogClient().capture({ distinctId: "admin", event: "donation_admin_test_popup" });
getPostHogClient().capture({
distinctId: "admin",
event: "donation_admin_test_popup",
});
sse.donate.pub("ping", {
id: "test",
@@ -24,7 +27,10 @@ export async function testPopup() {
export async function reloadWidget() {
if (!(await adminCheck())) throw new Error("Unauthorized");
getPostHogClient().capture({ distinctId: "admin", event: "donation_admin_widget_reload" });
getPostHogClient().capture({
distinctId: "admin",
event: "donation_admin_widget_reload",
});
sse.donate.pub("refresh", null);
}
@@ -38,7 +44,11 @@ export async function resendPopup(id: string) {
.returning();
if (!sub) throw new Error("not found");
getPostHogClient().capture({ distinctId: "admin", event: "donation_admin_resend", properties: { id } });
getPostHogClient().capture({
distinctId: "admin",
event: "donation_admin_resend",
properties: { id },
});
sse.donate.pub("ping", {
...sub,
+27 -12
View File
@@ -7,7 +7,7 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { donations } from "@/lib/db/schema";
import type { donations } from "@/lib/db/schema";
import { cn } from "@/lib/utils";
import type { ColumnDef } from "@tanstack/react-table";
import {
@@ -66,16 +66,26 @@ const columns: ColumnDef<typeof donations.$inferSelect>[] = [
return (
<div className="flex gap-1">
<SimpleTooltip text="Send test popup">
<ActionButton variant="outline" size="icon-sm" action={() => { posthog.capture("donation_admin_test_popup"); return testPopup(); }}>
<ActionButton
variant="outline"
size="icon-sm"
action={() => {
posthog.capture("donation_admin_test_popup");
return testPopup();
}}
>
<BugPlay />
</ActionButton>
</SimpleTooltip>
<SimpleTooltip text="Reload all widget">
<ActionButton
variant="outline"
size="icon-sm"
action={() => { posthog.capture("donation_admin_widget_reload"); return reloadWidget(); }}
>
<ActionButton
variant="outline"
size="icon-sm"
action={() => {
posthog.capture("donation_admin_widget_reload");
return reloadWidget();
}}
>
<ChevronsLeftRightEllipsis />
</ActionButton>
</SimpleTooltip>
@@ -92,11 +102,16 @@ const columns: ColumnDef<typeof donations.$inferSelect>[] = [
)}
>
<SimpleTooltip text="Re-send Popup">
<ActionButton
variant="outline"
size="icon-sm"
action={() => { posthog.capture("donation_admin_resend", { id: row.row.original.id }); return resendPopup(row.row.original.id); }}
>
<ActionButton
variant="outline"
size="icon-sm"
action={() => {
posthog.capture("donation_admin_resend", {
id: row.row.original.id,
});
return resendPopup(row.row.original.id);
}}
>
<MessageCircleWarning />
</ActionButton>
</SimpleTooltip>
+24 -4
View File
@@ -113,7 +113,15 @@ export default async function () {
const buffer = Buffer.from(arrayBuffer);
const processed = await checkSlip(buffer, $.slip.type, $.amount);
if (!processed.success) {
ph.capture({ distinctId, event: "donation_slip_check_failed", properties: { amount: $.amount, code: processed.code, message: processed.message } });
ph.capture({
distinctId,
event: "donation_slip_check_failed",
properties: {
amount: $.amount,
code: processed.code,
message: processed.message,
},
});
return {
error: {
where: "slip",
@@ -135,7 +143,11 @@ export default async function () {
return [{ id: "conflict" }];
});
if (check.id === "conflict") {
ph.capture({ distinctId, event: "donation_slip_conflict", properties: { amount: $.amount } });
ph.capture({
distinctId,
event: "donation_slip_conflict",
properties: { amount: $.amount },
});
return { error: { where: "slip", what: "สลิปนี้ถูกใช้ไปแล้ว" } };
}
} else {
@@ -158,7 +170,11 @@ export default async function () {
.then((r) => r.json())
.catch((e) => e);
if (!res.success) {
ph.capture({ distinctId, event: "donation_payment_failed", properties: { amount: $.amount, message: res.message } });
ph.capture({
distinctId,
event: "donation_payment_failed",
properties: { amount: $.amount, message: res.message },
});
return { error: { where: "link", what: res.message } };
}
}
@@ -196,7 +212,11 @@ export default async function () {
.catch(() => "conflict");
if (res === "conflict") {
tx.rollback();
ph.capture({ distinctId, event: "donation_artifact_conflict", properties: { amount: $.amount, uid: $.uid } });
ph.capture({
distinctId,
event: "donation_artifact_conflict",
properties: { amount: $.amount, uid: $.uid },
});
return { error: { where: "uid", what: "ไม่สามารถสร้างคิวลัดได้" } };
}
}
@@ -12,7 +12,7 @@ import {
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import type { Input } from "@/components/ui/input";
import { Kbd } from "@/components/ui/kbd";
import { Label } from "@/components/ui/label";
import {
@@ -32,7 +32,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SlipokResponse } from "../../../api";
import type { SlipokResponse } from "../../../api";
import { getDiscordUsers } from "@/app/api/discord/users/api";
import { th } from "zod/v4/locales";
+2 -1
View File
@@ -38,6 +38,7 @@ import {
import { shared } from "@/lib/comms";
import { sse } from "@/lib/db/sse-endpoints";
import { cn } from "@/lib/utils";
import type { getEndgameConfig } from "../api";
import { setFree, setLimit, toggleCheck, toggleLock } from "../api";
export function SidebarLink({
@@ -92,7 +93,7 @@ export function LimitManager({
length,
...props
}: React.ComponentProps<typeof SidebarMenu> & {
config: Awaited<ReturnType<typeof import("../api").getEndgameConfig>>;
config: Awaited<ReturnType<typeof getEndgameConfig>>;
length: number;
}) {
const [dialog, setDialog] = useState(1);
+6 -2
View File
@@ -3,7 +3,7 @@ import { donations } from "@/lib/db/schema";
import { sse } from "@/lib/db/sse-endpoints";
import { fileToDataUrl } from "@/lib/utils";
import { and, asc, not, sql } from "drizzle-orm";
import { NextRequest } from "next/server";
import type { NextRequest } from "next/server";
import { getPostHogClient } from "@/lib/posthog-server";
export async function PATCH(req: NextRequest) {
@@ -24,7 +24,11 @@ export async function PATCH(req: NextRequest) {
.limit(1)
.orderBy(asc(donations.id));
if (!havent) return;
getPostHogClient().capture({ distinctId: String(havent.id), event: "donation_resent", properties: { donation_id: havent.id, amount: havent.amount } });
getPostHogClient().capture({
distinctId: String(havent.id),
event: "donation_resent",
properties: { donation_id: havent.id, amount: havent.amount },
});
sse.donate.pub("ping", {
...havent,
message: havent.message ?? "",
+1
View File
@@ -26,6 +26,7 @@ export default function Providers({ children }: { children: React.ReactNode }) {
export function VersionCheck({ headless = false }: { headless?: boolean }) {
const ver = useRef<string>("");
// oxlint-disable-next-line typescript/unbound-method
const { emit } = use(IccContext);
const [debug, setDebug] = shared.state("debug");
const [, setConnected] = shared.state("connected");
+10 -2
View File
@@ -6,7 +6,11 @@ import { eq } from "drizzle-orm";
import { getPostHogClient } from "@/lib/posthog-server";
export async function markRunning(id: string) {
getPostHogClient().capture({ distinctId: id, event: "donation_widget_state_playing", properties: { donation_id: id } });
getPostHogClient().capture({
distinctId: id,
event: "donation_widget_state_playing",
properties: { donation_id: id },
});
await db
.update(donations)
@@ -15,7 +19,11 @@ export async function markRunning(id: string) {
}
export async function markDone(id: string) {
getPostHogClient().capture({ distinctId: id, event: "donation_widget_state_shown", properties: { donation_id: id } });
getPostHogClient().capture({
distinctId: id,
event: "donation_widget_state_shown",
properties: { donation_id: id },
});
await db.update(donations).set({ sent: true }).where(eq(donations.id, id));
}
+15 -5
View File
@@ -48,10 +48,15 @@ export default function () {
(tts.onerror = () => r(false))
),
);
if (!ttsAvailable) posthog.capture("donation_widget_tts_failed", { amount: data.amount });
if (!ttsAvailable)
posthog.capture("donation_widget_tts_failed", { amount: data.amount });
console.log("Transitioning in");
setMounted(true);
posthog.capture("donation_widget_displayed", { amount: data.amount, name_length: data.name.length, has_image: !!data.image });
posthog.capture("donation_widget_displayed", {
amount: data.amount,
name_length: data.name.length,
has_image: !!data.image,
});
console.log("Running SFX");
if (sfx.current) {
const player = sfx.current;
@@ -64,7 +69,10 @@ export default function () {
if (ttsAvailable) await new Promise((r) => (tts.onended = r));
console.log("Done");
markDone(data.id);
posthog.capture("donation_widget_animation_end", { amount: data.amount, tts_available: ttsAvailable });
posthog.capture("donation_widget_animation_end", {
amount: data.amount,
tts_available: ttsAvailable,
});
await new Promise((r) => setTimeout(r, 3000));
setMounted(false);
await new Promise((r) => setTimeout(r, 1200));
@@ -98,7 +106,9 @@ export default function () {
} catch {
console.error("Heartbeat timed out");
setFailed((x) => x + 1);
posthog.capture("donation_widget_heartbeat_failure", { fail_count: failed + 1 });
posthog.capture("donation_widget_heartbeat_failure", {
fail_count: failed + 1,
});
if (failed > 12) location.reload();
}
}, 10000);
@@ -111,7 +121,7 @@ export default function () {
// amount: 1,
// message: `เสื้อดำเด้าหน่อย`,
// });
}, []);
}, [failed]);
return (
<>
+17
View File
@@ -0,0 +1,17 @@
"use server";
import { db } from "@/lib/db";
import { donations } from "@/lib/db/schema";
import { asc, desc } from "drizzle-orm";
export async function getTopDonate() {
const [top] = await db
.select({
name: donations.name,
amount: donations.amount,
})
.from(donations)
.limit(1)
.orderBy(desc(donations.amount), asc(donations.id));
return top;
}
+60
View File
@@ -0,0 +1,60 @@
"use client";
import { sse } from "@/lib/db/sse-endpoints";
import { useEffect, useRef, useState } from "react";
import { getTopDonate } from "./api";
type Data = Awaited<ReturnType<typeof getTopDonate>>;
export default function TopDonateWidget() {
const [top, setTop] = useState<Data>();
const textRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
async function update() {
void getTopDonate().then(setTop);
}
queueMicrotask(update);
return sse.donate.subMany({
update,
ping: update,
refresh: () => location.reload(),
}).clean;
}, []);
// the most idiomatic way possible but it works
useEffect(() => {
if (!textRef.current) return;
var fontSize = 48,
count = 0;
const el = textRef.current;
const interval = setInterval(() => {
console.log(el.scrollWidth, el.clientWidth);
fontSize = (fontSize * el.clientWidth) / el.scrollWidth;
el.style.fontSize = `${fontSize}px`;
count++;
if (count > 20) clearInterval(interval);
}, 100);
return () => clearInterval(interval);
}, [textRef, top]);
return top ? (
<div className="grid grid-cols-[40px_minmax(0,1fr)_max-content] w-94.5 max-w-94.5 h-18.75 *:self-center gap-1 text-5xl font-semibold bg-black/75 rounded-full p-2 px-3 text-[#FFBA00] border-4 border-white">
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="currentColor"
className="size-10 shrink-0"
>
<path d="M200-160v-80h560v80H200Zm0-140-51-321q-2 0-4.5.5t-4.5.5q-25 0-42.5-17.5T80-680q0-25 17.5-42.5T140-740q25 0 42.5 17.5T200-680q0 7-1.5 13t-3.5 11l125 56 125-171q-11-8-18-21t-7-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820q0 15-7 28t-18 21l125 171 125-56q-2-5-3.5-11t-1.5-13q0-25 17.5-42.5T820-740q25 0 42.5 17.5T880-680q0 25-17.5 42.5T820-620q-2 0-4.5-.5t-4.5-.5l-51 321H200Zm68-80h424l26-167-105 46-133-183-133 183-105-46 26 167Zm212 0Z" />
</svg>
<div ref={textRef} className="min-w-0">
{top.name}
</div>
<div className="text-white">{top.amount}฿</div>
</div>
) : (
""
);
}
+1 -1
View File
@@ -343,4 +343,4 @@ process
.addListener("uncaughtException", console.error)
.addListener("unhandledRejection", console.error);
process.on(15 as unknown as "SIGTERM", process.exit);
process.on(15 as unknown as "SIGTERM", () => process.exit());
+15
View File
@@ -92,6 +92,7 @@
"drizzle-kit": "^0.31.8",
"oxfmt": "^0.52.0",
"oxlint": "^1.67.0",
"oxlint-tsgolint": "^0.23.0",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
@@ -467,6 +468,18 @@
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw=="],
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw=="],
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA=="],
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw=="],
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA=="],
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw=="],
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw=="],
"@oxlint/binding-android-arm64": ["@oxlint/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ=="],
@@ -1085,6 +1098,8 @@
"oxlint": ["[email protected]", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.67.0", "@oxlint/binding-android-arm64": "1.67.0", "@oxlint/binding-darwin-arm64": "1.67.0", "@oxlint/binding-darwin-x64": "1.67.0", "@oxlint/binding-freebsd-x64": "1.67.0", "@oxlint/binding-linux-arm-gnueabihf": "1.67.0", "@oxlint/binding-linux-arm-musleabihf": "1.67.0", "@oxlint/binding-linux-arm64-gnu": "1.67.0", "@oxlint/binding-linux-arm64-musl": "1.67.0", "@oxlint/binding-linux-ppc64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-musl": "1.67.0", "@oxlint/binding-linux-s390x-gnu": "1.67.0", "@oxlint/binding-linux-x64-gnu": "1.67.0", "@oxlint/binding-linux-x64-musl": "1.67.0", "@oxlint/binding-openharmony-arm64": "1.67.0", "@oxlint/binding-win32-arm64-msvc": "1.67.0", "@oxlint/binding-win32-ia32-msvc": "1.67.0", "@oxlint/binding-win32-x64-msvc": "1.67.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ=="],
"oxlint-tsgolint": ["[email protected]", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.23.0", "@oxlint-tsgolint/darwin-x64": "0.23.0", "@oxlint-tsgolint/linux-arm64": "0.23.0", "@oxlint-tsgolint/linux-x64": "0.23.0", "@oxlint-tsgolint/win32-arm64": "0.23.0", "@oxlint-tsgolint/win32-x64": "0.23.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA=="],
"p-retry": ["[email protected]", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"pathe": ["[email protected]", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+3 -2
View File
@@ -2,9 +2,10 @@
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { MaybePromise } from "bun";
import type { MaybePromise } from "bun";
import { Check } from "lucide-react";
import { ComponentProps, ReactNode, useEffect, useState } from "react";
import type { ComponentProps, ReactNode} from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
export function ActionButton({
+1
View File
@@ -146,6 +146,7 @@ export function CdnChooser({
if (!file) return setValue("");
setMeta(file);
})();
// oxlint-disable-next-line react-hooks/exhaustive-deps
}, [externalValue]);
async function choose() {
+1
View File
@@ -66,6 +66,7 @@ export function ComboBox({
</CommandItem>
),
})),
// oxlint-disable-next-line react-hooks/exhaustive-deps
[data],
);
const queryLower = query.toLowerCase();
+1
View File
@@ -224,6 +224,7 @@ export default function Component({
setZoom(1);
}
previousValueRef.current = value;
// oxlint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return (
+1
View File
@@ -1,3 +1,4 @@
// oxlint-disable react-hooks/exhaustive-deps
"use client";
import * as React from "react";
+1 -1
View File
@@ -173,7 +173,7 @@ function ChartTooltipContent({
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
"border-border/50 bg-background grid min-w-32 items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
+3
View File
@@ -108,6 +108,7 @@ export default function IccProvider({
(l) => l !== listener,
);
},
// oxlint-disable-next-line typescript/no-useless-default-assignment
emit(k, value = undefined) {
(listeners.current[k] || []).map((l) =>
l(...([value] as Parameters<typeof l>)),
@@ -188,6 +189,7 @@ export const shared = {
const { [k]: _, ...rest } = comms.get("inspect") || {};
comms.set("inspect", rest);
},
// oxlint-disable-next-line react-hooks/exhaustive-deps
[],
);
// value updater
@@ -197,6 +199,7 @@ export const shared = {
...obj,
[k]: typeof v === "string" ? v : JSON.stringify(v),
});
// oxlint-disable-next-line react-hooks/exhaustive-deps
}, [v]);
},
emit<K extends keyof SharedSignals>(
+1 -1
View File
@@ -1,4 +1,4 @@
import { SlipokResponse } from "@/app/(ui)/rubgram/api";
import type { SlipokResponse } from "@/app/(ui)/rubgram/api";
const { SLIPOK_API_URL, SLIPOK_API_KEY } = process.env as Record<
string,
+1
View File
@@ -106,6 +106,7 @@
"drizzle-kit": "^0.31.8",
"oxfmt": "^0.52.0",
"oxlint": "^1.67.0",
"oxlint-tsgolint": "^0.23.0",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3"