feat(donate): add donate UI
- rework form core and migrate all old usages
This commit is contained in:
@@ -27,5 +27,8 @@ WEBHOOK_RUBGRAM_SUBMIT=YOUR_WEBHOOK_RUBGRAM_SUBMIT # For Rubgram submit
|
|||||||
SLIPOK_API_URL=YOUR_SLIPOK_API_URL
|
SLIPOK_API_URL=YOUR_SLIPOK_API_URL
|
||||||
SLIPOK_API_KEY=YOUR_SLIPOK_API_KEY
|
SLIPOK_API_KEY=YOUR_SLIPOK_API_KEY
|
||||||
|
|
||||||
|
# Gemini TTS API (Donate)
|
||||||
|
GEMINI_TTS_API_KEY=YOUR_GEMINI_TTS_API_KEY
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
ENVIRONMENT=production
|
ENVIRONMENT=production
|
||||||
@@ -2,7 +2,6 @@ import { desc, eq } from "drizzle-orm";
|
|||||||
import { ArrowRight, Save, Trash2 } from "lucide-react";
|
import { ArrowRight, Save, Trash2 } from "lucide-react";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
|
||||||
import { CdnChooser } from "@/components/chooser";
|
import { CdnChooser } from "@/components/chooser";
|
||||||
import { FormAction, FormInput, FormProvider } from "@/components/form";
|
import { FormAction, FormInput, FormProvider } from "@/components/form";
|
||||||
import { ModalBase } from "@/components/modal";
|
import { ModalBase } from "@/components/modal";
|
||||||
@@ -37,17 +36,7 @@ export default async function CharacterEditPage({
|
|||||||
if (!char) redirect("/admin/char");
|
if (!char) redirect("/admin/char");
|
||||||
|
|
||||||
async function submit(
|
async function submit(
|
||||||
form: TypedFormData<{
|
form: FormData,
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
element: (typeof elementEnum.enumValues)[number];
|
|
||||||
stars: "4" | "5";
|
|
||||||
image: string;
|
|
||||||
order: string;
|
|
||||||
version: string;
|
|
||||||
weapon: string;
|
|
||||||
amber: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
"use server";
|
"use server";
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
@@ -72,14 +61,14 @@ export default async function CharacterEditPage({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
name: form.get("name")!,
|
name: form.get("name") as string,
|
||||||
vision: form.get("element")!,
|
vision: form.get("element") as string,
|
||||||
stars: parseInt(form.get("stars") || "0", 10) as 4 | 5,
|
stars: parseInt((form.get("stars") as string) || "0", 10) as 4 | 5,
|
||||||
image: form.get("image")!,
|
image: form.get("image") as string,
|
||||||
order: parseInt(form.get("order")!, 10),
|
order: parseInt(form.get("order") as string, 10),
|
||||||
version: form.get("version")!,
|
version: form.get("version") as string,
|
||||||
weapon: form.get("weapon")!,
|
weapon: form.get("weapon") as string,
|
||||||
amber: form.get("amber")!,
|
amber: form.get("amber") as string,
|
||||||
};
|
};
|
||||||
await db.update(characters).set(data).where(eq(characters.id, charId));
|
await db.update(characters).set(data).where(eq(characters.id, charId));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { desc, sql } from "drizzle-orm";
|
|||||||
import { ArrowRight, PlusIcon } from "lucide-react";
|
import { ArrowRight, PlusIcon } from "lucide-react";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
|
||||||
import { CdnChooser } from "@/components/chooser";
|
import { CdnChooser } from "@/components/chooser";
|
||||||
import { FormAction, FormInput, FormProvider } from "@/components/form";
|
import { FormAction, FormInput, FormProvider } from "@/components/form";
|
||||||
import { ModalBase } from "@/components/modal";
|
import { ModalBase } from "@/components/modal";
|
||||||
@@ -34,17 +33,7 @@ export default async function CharacterCreatePage() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
async function submit(
|
async function submit(
|
||||||
form: TypedFormData<{
|
form: FormData,
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
element: (typeof elementEnum.enumValues)[number];
|
|
||||||
stars: "4" | "5";
|
|
||||||
image: string;
|
|
||||||
order: string;
|
|
||||||
version: string;
|
|
||||||
weapon: string;
|
|
||||||
amber: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
"use server";
|
"use server";
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
@@ -69,15 +58,15 @@ export default async function CharacterCreatePage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
id: form.get("id")!,
|
id: form.get("id") as string,
|
||||||
name: form.get("name")!,
|
name: form.get("name") as string,
|
||||||
vision: form.get("element")!,
|
vision: form.get("element") as string,
|
||||||
stars: parseInt(form.get("stars") || "0", 10) as 4 | 5,
|
stars: parseInt((form.get("stars") as string) || "0", 10) as 4 | 5,
|
||||||
image: form.get("image")!,
|
image: form.get("image") as string,
|
||||||
order: parseInt(form.get("order")!, 10),
|
order: parseInt(form.get("order") as string, 10),
|
||||||
version: form.get("version")!,
|
version: form.get("version") as string,
|
||||||
weapon: form.get("weapon")!,
|
weapon: form.get("weapon") as string,
|
||||||
amber: form.get("amber")!,
|
amber: form.get("amber") as string,
|
||||||
};
|
};
|
||||||
await db.insert(characters).values(data);
|
await db.insert(characters).values(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { eq } from "drizzle-orm";
|
|||||||
import { SaveIcon, Trash2 } from "lucide-react";
|
import { SaveIcon, Trash2 } from "lucide-react";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
|
||||||
import { CdnChooser } from "@/components/chooser";
|
import { CdnChooser } from "@/components/chooser";
|
||||||
import {
|
import {
|
||||||
FormAction,
|
FormAction,
|
||||||
@@ -29,12 +28,7 @@ export default async function GuideEditPage({
|
|||||||
if (!guide) notFound();
|
if (!guide) notFound();
|
||||||
|
|
||||||
async function submit(
|
async function submit(
|
||||||
form: TypedFormData<{
|
form: FormData,
|
||||||
name: string;
|
|
||||||
link: string;
|
|
||||||
image: string;
|
|
||||||
order: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
"use server";
|
"use server";
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
@@ -49,10 +43,10 @@ export default async function GuideEditPage({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
name: form.get("name")!,
|
name: form.get("name") as string,
|
||||||
image: form.get("image")!,
|
image: form.get("image") as string,
|
||||||
link: form.get("link")!,
|
link: form.get("link") as string,
|
||||||
order: parseInt(form.get("order")!, 10),
|
order: parseInt(form.get("order") as string, 10),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
new URL(data.link!);
|
new URL(data.link!);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { sql } from "drizzle-orm";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
|
||||||
import { CdnChooser } from "@/components/chooser";
|
import { CdnChooser } from "@/components/chooser";
|
||||||
import {
|
import {
|
||||||
FormAction,
|
FormAction,
|
||||||
@@ -29,12 +28,7 @@ export default async function GuideCreatePage() {
|
|||||||
})
|
})
|
||||||
.from(guides);
|
.from(guides);
|
||||||
async function submit(
|
async function submit(
|
||||||
form: TypedFormData<{
|
form: FormData,
|
||||||
name: string;
|
|
||||||
link: string;
|
|
||||||
image: string;
|
|
||||||
order: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
"use server";
|
"use server";
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
@@ -49,10 +43,10 @@ export default async function GuideCreatePage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
name: form.get("name")!,
|
name: form.get("name") as string,
|
||||||
image: form.get("image")!,
|
image: form.get("image") as string,
|
||||||
link: form.get("link")!,
|
link: form.get("link") as string,
|
||||||
order: parseInt(form.get("order")!, 10),
|
order: parseInt(form.get("order") as string, 10),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
new URL(data.link);
|
new URL(data.link);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { ArrowRight, PlusIcon, SaveIcon, Trash2 } from "lucide-react";
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
|
||||||
import { CdnChooser } from "@/components/chooser";
|
import { CdnChooser } from "@/components/chooser";
|
||||||
import { DatePicker } from "@/components/date";
|
import { DatePicker } from "@/components/date";
|
||||||
import {
|
import {
|
||||||
@@ -44,15 +43,7 @@ export default async function TlVersionEditPage({
|
|||||||
if (!ver) notFound();
|
if (!ver) notFound();
|
||||||
|
|
||||||
async function submit(
|
async function submit(
|
||||||
form: TypedFormData<{
|
form: FormData,
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
order: string;
|
|
||||||
from: string;
|
|
||||||
image: string;
|
|
||||||
disclaimer: string;
|
|
||||||
deprecates: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
"use server";
|
"use server";
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
@@ -73,13 +64,13 @@ export default async function TlVersionEditPage({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
id: form.get("id")!,
|
id: form.get("id") as string,
|
||||||
name: form.get("name")!,
|
name: form.get("name") as string,
|
||||||
image: form.get("image")!,
|
image: form.get("image") as string,
|
||||||
disclaimer: form.get("disclaimer")!,
|
disclaimer: form.get("disclaimer") as string,
|
||||||
from: form.get("from")!,
|
from: form.get("from") as string,
|
||||||
deprecates: form.get("deprecates")!,
|
deprecates: form.get("deprecates") as string,
|
||||||
order: parseInt(form.get("order")!, 10),
|
order: parseInt(form.get("order") as string, 10),
|
||||||
type: typeId,
|
type: typeId,
|
||||||
};
|
};
|
||||||
await db
|
await db
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { ArrowRight, PlusIcon } from "lucide-react";
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
|
||||||
import { CdnChooser } from "@/components/chooser";
|
import { CdnChooser } from "@/components/chooser";
|
||||||
import { DatePicker } from "@/components/date";
|
import { DatePicker } from "@/components/date";
|
||||||
import {
|
import {
|
||||||
@@ -59,15 +58,7 @@ export default async function TlVersionCreatePage({
|
|||||||
if (!type) notFound();
|
if (!type) notFound();
|
||||||
|
|
||||||
async function submit(
|
async function submit(
|
||||||
form: TypedFormData<{
|
form: FormData,
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
order: string;
|
|
||||||
from: string;
|
|
||||||
image: string;
|
|
||||||
disclaimer: string;
|
|
||||||
deprecates: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
"use server";
|
"use server";
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
@@ -88,13 +79,13 @@ export default async function TlVersionCreatePage({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
id: form.get("id")!,
|
id: form.get("id") as string,
|
||||||
name: form.get("name")!,
|
name: form.get("name") as string,
|
||||||
image: form.get("image")!,
|
image: form.get("image") as string,
|
||||||
disclaimer: form.get("disclaimer")!,
|
disclaimer: form.get("disclaimer") as string,
|
||||||
from: form.get("from")!,
|
from: form.get("from") as string,
|
||||||
deprecates: form.get("deprecates")!,
|
deprecates: form.get("deprecates") as string,
|
||||||
order: parseInt(form.get("order")!, 10),
|
order: parseInt(form.get("order") as string, 10),
|
||||||
type: typeId,
|
type: typeId,
|
||||||
};
|
};
|
||||||
await db.insert(tierlistVersions).values(data);
|
await db.insert(tierlistVersions).values(data);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { eq } from "drizzle-orm";
|
|||||||
import { SaveIcon, Trash2 } from "lucide-react";
|
import { SaveIcon, Trash2 } from "lucide-react";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
|
||||||
import { CdnChooser } from "@/components/chooser";
|
import { CdnChooser } from "@/components/chooser";
|
||||||
import {
|
import {
|
||||||
FormAction,
|
FormAction,
|
||||||
@@ -34,13 +33,7 @@ export default async function TlTypeEditPage({
|
|||||||
if (!type) notFound();
|
if (!type) notFound();
|
||||||
|
|
||||||
async function submit(
|
async function submit(
|
||||||
form: TypedFormData<{
|
form: FormData,
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
order: string;
|
|
||||||
image: string;
|
|
||||||
mode: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
"use server";
|
"use server";
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
@@ -55,11 +48,11 @@ export default async function TlTypeEditPage({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
id: form.get("id")!,
|
id: form.get("id") as string,
|
||||||
name: form.get("name")!,
|
name: form.get("name") as string,
|
||||||
image: form.get("image")!,
|
image: form.get("image") as string,
|
||||||
order: parseInt(form.get("order")!, 10),
|
order: parseInt(form.get("order") as string, 10),
|
||||||
mode: form.get("mode")!,
|
mode: form.get("mode") as string,
|
||||||
};
|
};
|
||||||
await db
|
await db
|
||||||
.update(tierlistTypes)
|
.update(tierlistTypes)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { sql } from "drizzle-orm";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
|
||||||
import { CdnChooser } from "@/components/chooser";
|
import { CdnChooser } from "@/components/chooser";
|
||||||
import { FormAction, FormInput, FormProvider } from "@/components/form";
|
import { FormAction, FormInput, FormProvider } from "@/components/form";
|
||||||
import { ModalBase } from "@/components/modal";
|
import { ModalBase } from "@/components/modal";
|
||||||
@@ -22,13 +21,7 @@ export default async function TlTypeCreatePage() {
|
|||||||
.from(tierlistTypes);
|
.from(tierlistTypes);
|
||||||
|
|
||||||
async function submit(
|
async function submit(
|
||||||
form: TypedFormData<{
|
form: FormData,
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
order: string;
|
|
||||||
image: string;
|
|
||||||
mode: string;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
"use server";
|
"use server";
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
@@ -43,11 +36,11 @@ export default async function TlTypeCreatePage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = {
|
data = {
|
||||||
id: form.get("id")!,
|
id: form.get("id") as string,
|
||||||
name: form.get("name")!,
|
name: form.get("name") as string,
|
||||||
image: form.get("image")!,
|
image: form.get("image") as string,
|
||||||
mode: form.get("mode")!,
|
mode: form.get("mode") as string,
|
||||||
order: parseInt(form.get("order")!, 10),
|
order: parseInt(form.get("order") as string, 10),
|
||||||
};
|
};
|
||||||
await db.insert(tierlistTypes).values(data);
|
await db.insert(tierlistTypes).values(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SimpleTooltip } from "@/components/tooltip";
|
||||||
|
import { Kbd } from "@/components/ui/kbd";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { X, FileSearch2 } from "lucide-react";
|
||||||
|
import { useRef, useState, useEffect } from "react";
|
||||||
|
import { b2sClient } from "../admin/cdn/table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export function DonateImageUpload({
|
||||||
|
value: externalValue,
|
||||||
|
onValueChange,
|
||||||
|
}: {
|
||||||
|
value?: File | null | undefined;
|
||||||
|
onValueChange?: (value: File | null) => void;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
const [value, setValue] = useState<File | undefined | null>(externalValue);
|
||||||
|
const isSelected = !!value?.name;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
typeof externalValue === "object" &&
|
||||||
|
externalValue &&
|
||||||
|
Object.keys(externalValue).length === 0
|
||||||
|
)
|
||||||
|
// ignore {}
|
||||||
|
return;
|
||||||
|
setValue(externalValue);
|
||||||
|
}, [externalValue]);
|
||||||
|
|
||||||
|
function choose() {
|
||||||
|
ref.current?.click();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={isSelected ? undefined : () => ref.current?.click()}
|
||||||
|
className={cn(
|
||||||
|
"h-full w-full aspect-square justify-between",
|
||||||
|
isSelected && "text-emerald-300!",
|
||||||
|
)}
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isSelected ? (
|
||||||
|
<span onClick={isSelected ? choose : undefined} className="truncate">
|
||||||
|
{value.name} <Kbd>{b2sClient(Number(value.size))}</Kbd>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
"a"
|
||||||
|
)}
|
||||||
|
{isSelected ? (
|
||||||
|
<SimpleTooltip text="Unselect">
|
||||||
|
<X
|
||||||
|
className="hover:text-red-500 pointer-events-auto!"
|
||||||
|
onClick={() => {
|
||||||
|
setValue(null);
|
||||||
|
onValueChange?.(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SimpleTooltip>
|
||||||
|
) : (
|
||||||
|
<FileSearch2 />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
hidden
|
||||||
|
onChange={() => {
|
||||||
|
const file = ref.current?.files?.[0] || null;
|
||||||
|
setValue(file);
|
||||||
|
onValueChange?.(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { formParse } from "@/components/form-submit";
|
||||||
|
import {
|
||||||
|
FormAction,
|
||||||
|
FormChoice,
|
||||||
|
FormInput,
|
||||||
|
FormProvider,
|
||||||
|
FormRow,
|
||||||
|
FormTab,
|
||||||
|
} from "@/components/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
CurrencyInput,
|
||||||
|
SlipUpload,
|
||||||
|
} from "../rubgram/admin/@modal/manual/client";
|
||||||
|
import Image from "@/components/image";
|
||||||
|
import PromptpayImage from "#/assets/promptpay.jpg";
|
||||||
|
import { DownloadButton } from "../rubgram/client";
|
||||||
|
import { QrCodeIcon, SendIcon } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import TruemoneyIcon from "#/assets/tmn.webp";
|
||||||
|
import z from "zod";
|
||||||
|
import { th } from "zod/v4/locales";
|
||||||
|
import Cropper from "@/components/cropper";
|
||||||
|
|
||||||
|
z.config(th());
|
||||||
|
|
||||||
|
const Schema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().max(50, { error: "ชื่อยาวสุด 50 ตัวอักษร" }).optional(),
|
||||||
|
message: z
|
||||||
|
.string()
|
||||||
|
.max(500, { error: "ข้อความยาวสุด 200 ตัวอักษร" })
|
||||||
|
.optional(),
|
||||||
|
amount: z.coerce
|
||||||
|
.number({ error: "จำนวนต้องเป็นตัวเลข" })
|
||||||
|
.min(10, { error: "โดเนทขั้นต่ำ 10 บาท" }),
|
||||||
|
})
|
||||||
|
.and(
|
||||||
|
z.discriminatedUnion("type", [
|
||||||
|
z.object({
|
||||||
|
type: z.literal("tmn"),
|
||||||
|
link: z.httpUrl(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("pp"),
|
||||||
|
slip: z.file(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
async function submit(data: FormData) {
|
||||||
|
"use server";
|
||||||
|
const { $, error } = formParse(Schema, data);
|
||||||
|
if (error) return { error };
|
||||||
|
|
||||||
|
return { toast: "ระบบยังไม่เสร็จ" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-svh">
|
||||||
|
<div className="p-5 bg-card border rounded-md w-full max-w-md">
|
||||||
|
<FormProvider id="tip" inDialog={false} onSubmit={submit}>
|
||||||
|
<FormRow>
|
||||||
|
<FormInput name="image" className="h-full">
|
||||||
|
<Cropper />
|
||||||
|
</FormInput>
|
||||||
|
<div className="grid gap-4 [&>label]:-mb-2 grow">
|
||||||
|
<FormInput name="name" label="ชื่อ" subLabel="ไม่จำเป็น">
|
||||||
|
<Input placeholder="Anonymous" />
|
||||||
|
</FormInput>
|
||||||
|
<FormInput name="amount" label="จำนวนโดเนท" subLabel="ขึ้นจอขั้นต่ำ 10 บาท">
|
||||||
|
<CurrencyInput placeholder="ขั้นต่ำ 1 บาท" />
|
||||||
|
</FormInput>
|
||||||
|
</div>
|
||||||
|
</FormRow>
|
||||||
|
<FormInput name="message" label="ข้อความ" subLabel="max. 500 ตัวอักษร">
|
||||||
|
<Textarea placeholder="ข้อความ" />
|
||||||
|
</FormInput>
|
||||||
|
<FormTab
|
||||||
|
label="วิธีการโอนเงิน"
|
||||||
|
name="type"
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<span className="flex gap-1 items-center">
|
||||||
|
<Image
|
||||||
|
src={TruemoneyIcon}
|
||||||
|
alt="Truemoney"
|
||||||
|
className="w-12 h-6 object-cover"
|
||||||
|
/>
|
||||||
|
TrueMoney
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
value: "tmn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<span className="flex gap-1 items-center">
|
||||||
|
<QrCodeIcon className="size-6" />
|
||||||
|
PromptPay
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
value: "pp",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormChoice value="tmn">
|
||||||
|
<FormInput name="link" label="ลิ้งค์อั่งเปา">
|
||||||
|
<Input placeholder="https://gift.truemoney.com/campaign/?v=..." />
|
||||||
|
</FormInput>
|
||||||
|
</FormChoice>
|
||||||
|
<FormChoice value="pp">
|
||||||
|
<div className="flex gap-2 w-full pb-2">
|
||||||
|
<Image
|
||||||
|
src={PromptpayImage}
|
||||||
|
alt="Promptpay QR Code"
|
||||||
|
className="rounded max-w-32 shrink-0"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col shrink-0 relative">
|
||||||
|
<span className="font-bold text-sm">บัญชีรับโดเนท</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
ผู้รับ: นาย พัชรพล พลพันธุ์
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
บัญชี: xxx-x-x8666-x
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
เลขที่อ้างอิง: 004999056945438
|
||||||
|
</span>
|
||||||
|
<DownloadButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormInput name="slip">
|
||||||
|
<SlipUpload />
|
||||||
|
</FormInput>
|
||||||
|
</FormChoice>
|
||||||
|
</FormTab>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button asChild>
|
||||||
|
<FormAction
|
||||||
|
disabled
|
||||||
|
type="submit"
|
||||||
|
loading={
|
||||||
|
<>
|
||||||
|
<Spinner />
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SendIcon />
|
||||||
|
ส่งเลยจัฟลูกพี่
|
||||||
|
</FormAction>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+13
-8
@@ -1,6 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Feather, Home, LoaderPinwheel, StickyNote, Table } from "lucide-react";
|
import {
|
||||||
|
Bitcoin,
|
||||||
|
Feather,
|
||||||
|
Home,
|
||||||
|
LoaderPinwheel,
|
||||||
|
StickyNote,
|
||||||
|
Table,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
@@ -11,7 +18,9 @@ export function Navbar() {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const mobile = useIsMobile();
|
const mobile = useIsMobile();
|
||||||
|
|
||||||
if (!/^\/(?:tl(?:\/[a-zA-Z0-9]+)?|rubgram|artifact|guide)$/gm.test(pathname))
|
if (
|
||||||
|
!/^\/(?:tl(?:\/[a-zA-Z0-9]+)?|rubgram|artifact|guide|tip)$/gm.test(pathname)
|
||||||
|
)
|
||||||
return "";
|
return "";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -32,12 +41,8 @@ export function Navbar() {
|
|||||||
href="/rubgram"
|
href="/rubgram"
|
||||||
/>
|
/>
|
||||||
<Item icon={<Table size={16} />} name="จัดเทียร์ลิสต์" href="/tl" />
|
<Item icon={<Table size={16} />} name="จัดเทียร์ลิสต์" href="/tl" />
|
||||||
<Item
|
<Item icon={<StickyNote size={16} />} name="ไกด์ตัวละคร" href="/guide" />
|
||||||
icon={<StickyNote size={16} />}
|
<Item icon={<Bitcoin size={16} />} name="โดเนทขึ้นจอ" href="/tip" last />
|
||||||
name="ไกด์ตัวละคร"
|
|
||||||
href="/guide"
|
|
||||||
last
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -29,7 +29,7 @@ export default function Home() {
|
|||||||
</center>
|
</center>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="flex flex-wrap my-10">
|
<div className="flex flex-wrap justify-evenly my-10 gap-5">
|
||||||
<HomeLink
|
<HomeLink
|
||||||
name="เสือกไอดีชาวบ้าน"
|
name="เสือกไอดีชาวบ้าน"
|
||||||
desc="ระบบลงคิวดูอาร์ติแฟกต์เกนชินในไลฟ์สตรีม"
|
desc="ระบบลงคิวดูอาร์ติแฟกต์เกนชินในไลฟ์สตรีม"
|
||||||
@@ -59,6 +59,12 @@ export default function Home() {
|
|||||||
href="/guide"
|
href="/guide"
|
||||||
logo={GuideLogo}
|
logo={GuideLogo}
|
||||||
/>
|
/>
|
||||||
|
{/* <HomeLink
|
||||||
|
name="Donate"
|
||||||
|
desc="โดเนทขึ้นไลฟ์สตรีม"
|
||||||
|
href="/tip"
|
||||||
|
logo={GuideLogo}
|
||||||
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
<footer className="text-muted-foreground h-full flex flex-col justify-end pb-3 md:justify-center">
|
<footer className="text-muted-foreground h-full flex flex-col justify-end pb-3 md:justify-center">
|
||||||
<div className="flex justify-between flex-col text-center gap-2 sm:flex-row sm:text-left">
|
<div className="flex justify-between flex-col text-center gap-2 sm:flex-row sm:text-left">
|
||||||
@@ -144,7 +150,7 @@ function HomeLink({
|
|||||||
<SimpleTooltip text={desc}>
|
<SimpleTooltip text={desc}>
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className={cn("flex flex-col p-10 mx-auto text-center", className)}
|
className={cn("flex flex-col p-10 text-center", className)}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={logo}
|
src={logo}
|
||||||
|
|||||||
@@ -24,14 +24,13 @@ import {
|
|||||||
MultiSelectValue,
|
MultiSelectValue,
|
||||||
} from "@/components/ui/multi-select";
|
} from "@/components/ui/multi-select";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Select, SelectTrigger } from "@/components/ui/select";
|
|
||||||
import { VirtualizedComboBox } from "@/components/combobox";
|
import { VirtualizedComboBox } from "@/components/combobox";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
|
||||||
export function CurrencyInput(props: ComponentProps<typeof Input>) {
|
export function CurrencyInput(props: ComponentProps<typeof Input>) {
|
||||||
return (
|
return (
|
||||||
<InputGroup className="max-w-xs">
|
<InputGroup>
|
||||||
<InputGroupInput placeholder="ฟรี" {...props} />
|
<InputGroupInput {...props} />
|
||||||
<InputGroupAddon align="inline-end">฿</InputGroupAddon>
|
<InputGroupAddon align="inline-end">฿</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
);
|
);
|
||||||
@@ -131,7 +130,7 @@ export function SlipUpload({
|
|||||||
{value.name} <Kbd>{b2sClient(Number(value.size))}</Kbd>
|
{value.name} <Kbd>{b2sClient(Number(value.size))}</Kbd>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
"Upload Slip"
|
"อัพโหลดสลิปโอนเงิน"
|
||||||
)}
|
)}
|
||||||
{isSelected ? (
|
{isSelected ? (
|
||||||
<SimpleTooltip text="Unselect">
|
<SimpleTooltip text="Unselect">
|
||||||
@@ -151,7 +150,6 @@ export function SlipUpload({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
required
|
|
||||||
hidden
|
hidden
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
const file = ref.current?.files?.[0] || null;
|
const file = ref.current?.files?.[0] || null;
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
endgameSubmissions,
|
endgameSubmissions,
|
||||||
endgameTypes,
|
endgameTypes,
|
||||||
} from "@/lib/db/schema";
|
} from "@/lib/db/schema";
|
||||||
import type { TypedFormData, TypedFormDataShape } from "../../../type";
|
|
||||||
import { CurrencyInput, ServiceSelect, SlipUpload, UserSelect } from "./client";
|
import { CurrencyInput, ServiceSelect, SlipUpload, UserSelect } from "./client";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { SlipokResponse } from "../../../api";
|
import { SlipokResponse } from "../../../api";
|
||||||
import { getDiscordUsers } from "@/app/api/discord/users/api";
|
import { getDiscordUsers } from "@/app/api/discord/users/api";
|
||||||
|
import { th } from "zod/v4/locales";
|
||||||
|
|
||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
@@ -48,14 +48,22 @@ const Schema = z.object({
|
|||||||
export default async function RgManualCreateModal() {
|
export default async function RgManualCreateModal() {
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
const types = await db.select().from(endgameTypes);
|
const types = await db.select().from(endgameTypes);
|
||||||
async function submit(form: TypedFormData<TypedFormDataShape>) {
|
async function submit(form: FormData) {
|
||||||
"use server";
|
"use server";
|
||||||
if (!(await adminCheck())) redirect("/login");
|
if (!(await adminCheck())) redirect("/login");
|
||||||
|
|
||||||
const raw = Object.fromEntries([...form.entries()]);
|
const raw = Object.fromEntries([...form.entries()]);
|
||||||
|
z.config(th());
|
||||||
const parsed = Schema.safeParse(raw);
|
const parsed = Schema.safeParse(raw);
|
||||||
console.log(raw, parsed);
|
console.log(raw, parsed);
|
||||||
if (!parsed.success) return { error: z.prettifyError(parsed.error) };
|
if (!parsed.success) {
|
||||||
|
return {
|
||||||
|
error: parsed.error.issues.map((issue) => ({
|
||||||
|
what: issue.message,
|
||||||
|
where: issue.path.join("."),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
const { data } = parsed;
|
const { data } = parsed;
|
||||||
|
|
||||||
const [{ id: slipId }] = await db
|
const [{ id: slipId }] = await db
|
||||||
@@ -101,7 +109,7 @@ export default async function RgManualCreateModal() {
|
|||||||
<Input placeholder="Dreamgineer" autoFocus />
|
<Input placeholder="Dreamgineer" autoFocus />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
<FormInput name="price" label="Price" className="flex-1">
|
<FormInput name="price" label="Price" className="flex-1">
|
||||||
<CurrencyInput />
|
<CurrencyInput placeholder="ฟรี" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormInput name="services" label="Services">
|
<FormInput name="services" label="Services">
|
||||||
|
|||||||
+8
-22
@@ -20,8 +20,6 @@ import {
|
|||||||
} from "@/lib/db/schema";
|
} from "@/lib/db/schema";
|
||||||
import { sse } from "@/lib/db/sse-endpoints";
|
import { sse } from "@/lib/db/sse-endpoints";
|
||||||
import { getPostHogClient } from "@/lib/posthog-server";
|
import { getPostHogClient } from "@/lib/posthog-server";
|
||||||
import type { TypedFormData } from "./type";
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
SLIPOK_API_URL,
|
SLIPOK_API_URL,
|
||||||
SLIPOK_API_KEY,
|
SLIPOK_API_KEY,
|
||||||
@@ -205,19 +203,12 @@ export async function getEndgameConfig() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EndgameFormData = TypedFormData<{
|
export async function submitEndgame(formData: FormData) {
|
||||||
name: string;
|
|
||||||
server: "as" | "eu" | "us" | "tw";
|
|
||||||
service: string;
|
|
||||||
user: string; // user snowflake
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export async function submitEndgame(formData: EndgameFormData) {
|
|
||||||
const { full, locked, limit, types, free } = await getEndgameConfig();
|
const { full, locked, limit, types, free } = await getEndgameConfig();
|
||||||
const name = formData.get("name");
|
const name = formData.get("name") as string;
|
||||||
const server = formData.get("server");
|
const server = formData.get("server") as "as" | "eu" | "us" | "tw";
|
||||||
const service = formData.getAll("service");
|
const service = formData.getAll("service") as string[];
|
||||||
const user = formData.get("user");
|
const user = formData.get("user") as string;
|
||||||
if (!name || !server || !service.length || !user)
|
if (!name || !server || !service.length || !user)
|
||||||
return "กรุณากรอกข้อมูลให้ครบถ้วน";
|
return "กรุณากรอกข้อมูลให้ครบถ้วน";
|
||||||
|
|
||||||
@@ -267,14 +258,9 @@ export async function submitEndgame(formData: EndgameFormData) {
|
|||||||
return queue;
|
return queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EndgamePaymentFormData = TypedFormData<{
|
export async function submitEndgamePayment(formData: FormData) {
|
||||||
sid: string;
|
const sid = formData.get("sid") as string;
|
||||||
slip: File;
|
const slip = formData.get("slip") as File;
|
||||||
}>;
|
|
||||||
|
|
||||||
export async function submitEndgamePayment(formData: EndgamePaymentFormData) {
|
|
||||||
const sid = formData.get("sid");
|
|
||||||
const slip = formData.get("slip");
|
|
||||||
if (!sid || !slip) return "กรุณาอัพโหลดสลิปให้ครบถ้วน";
|
if (!sid || !slip) return "กรุณาอัพโหลดสลิปให้ครบถ้วน";
|
||||||
|
|
||||||
const arrayBuffer = await slip.arrayBuffer();
|
const arrayBuffer = await slip.arrayBuffer();
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { useRouter } from "next/navigation";
|
|||||||
import posthog from "posthog-js";
|
import posthog from "posthog-js";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
type EndgameFormData,
|
|
||||||
type EndgamePaymentFormData,
|
|
||||||
submitEndgame,
|
submitEndgame,
|
||||||
submitEndgamePayment,
|
submitEndgamePayment,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
@@ -26,7 +24,7 @@ export function EndgameFormWrapper({
|
|||||||
);
|
);
|
||||||
|
|
||||||
({ registration: submitEndgame, payment: submitEndgamePayment })
|
({ registration: submitEndgame, payment: submitEndgamePayment })
|
||||||
[type](data as unknown as EndgameFormData & EndgamePaymentFormData)
|
[type](data)
|
||||||
.then((q) => {
|
.then((q) => {
|
||||||
if (typeof q === "string") return toast.error(q);
|
if (typeof q === "string") return toast.error(q);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Vendored
-31
@@ -1,31 +0,0 @@
|
|||||||
export type TypedFormDataShape = Record<
|
|
||||||
string,
|
|
||||||
string | Blob | File | (string | Blob | File)[]
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface TypedFormData<T extends TypedFormDataShape> {
|
|
||||||
append<K extends keyof T>(
|
|
||||||
key: K,
|
|
||||||
value: T[K] extends unknown[] ? T[K][number] : T[K],
|
|
||||||
): void;
|
|
||||||
|
|
||||||
set<K extends keyof T>(
|
|
||||||
key: K,
|
|
||||||
value: T[K] extends unknown[] ? T[K][number] : T[K],
|
|
||||||
): void;
|
|
||||||
|
|
||||||
get<K extends keyof T>(
|
|
||||||
key: K,
|
|
||||||
): T[K] extends unknown[] ? T[K][number] | null : T[K] | null;
|
|
||||||
|
|
||||||
getAll<K extends keyof T>(key: K): T[K] extends unknown[] ? T[K] : T[K][];
|
|
||||||
|
|
||||||
delete<K extends keyof T>(key: K): void;
|
|
||||||
|
|
||||||
has<K extends keyof T>(key: K): boolean;
|
|
||||||
|
|
||||||
entries(): ReturnType<FormData["entries"]>;
|
|
||||||
|
|
||||||
/** underlying raw FormData */
|
|
||||||
readonly raw: FormData;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { sse } from "@/lib/db/sse-endpoints";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export async function PATCH(req: NextRequest) {
|
||||||
|
const tag = Number(req.nextUrl.searchParams.get("tag") ?? "abc");
|
||||||
|
if (Number.isNaN(tag))
|
||||||
|
return Response.json({ error: "Invalid Tag" }, { status: 400 });
|
||||||
|
sse.donate.pub("heartbeat", tag);
|
||||||
|
return Response.json({ success: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { GoogleGenAI } from "@google/genai";
|
||||||
|
import wav from "wav";
|
||||||
|
import { Readable, PassThrough } from "stream";
|
||||||
|
import z from "zod";
|
||||||
|
import { env } from "process";
|
||||||
|
import { redis } from "@/lib/db/redis";
|
||||||
|
import { hash } from "bun";
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
message: z.string().max(1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
const voices = ["Zephyr", "Charon", "Fenrir", "Kore", "Sulafat"];
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const data = Schema.safeParse(
|
||||||
|
Object.fromEntries(new URL(request.url).searchParams.entries()),
|
||||||
|
);
|
||||||
|
if (data.error)
|
||||||
|
return new Response(z.prettifyError(data.error), { status: 400 });
|
||||||
|
const {
|
||||||
|
data: { message },
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const textHash = hash(message).toString(36);
|
||||||
|
const cached = await redis!.get(`tts:${textHash}`);
|
||||||
|
if (cached)
|
||||||
|
return new Response(pcmToWav(cached), {
|
||||||
|
headers: { "Content-Type": "audio/wav" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new GoogleGenAI({ apiKey: env.GEMINI_TTS_API_KEY });
|
||||||
|
|
||||||
|
const response = await client.models.generateContent({
|
||||||
|
model: "gemini-2.5-flash-preview-tts",
|
||||||
|
contents: [{ parts: [{ text: message }] }],
|
||||||
|
config: {
|
||||||
|
responseModalities: ["AUDIO"],
|
||||||
|
speechConfig: {
|
||||||
|
voiceConfig: {
|
||||||
|
prebuiltVoiceConfig: {
|
||||||
|
voiceName: voices[Math.floor(Math.random() * voices.length)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const audioData =
|
||||||
|
response.candidates?.[0].content?.parts?.[0].inlineData?.data;
|
||||||
|
if (!audioData)
|
||||||
|
return Response.json({ error: "Response is empty" }, { status: 500 });
|
||||||
|
queueMicrotask(() => {
|
||||||
|
redis!.setex(`tts:${textHash}`, 604800, audioData);
|
||||||
|
});
|
||||||
|
return new Response(pcmToWav(audioData), {
|
||||||
|
headers: { "Content-Type": "audio/wav" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pcmToWav(audioData: string) {
|
||||||
|
const audioBuffer = Buffer.from(audioData, "base64");
|
||||||
|
const pcmStream = Readable.from([audioBuffer]);
|
||||||
|
const writer = new wav.Writer({
|
||||||
|
channels: 1,
|
||||||
|
sampleRate: 24000,
|
||||||
|
bitDepth: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
const out = new PassThrough();
|
||||||
|
|
||||||
|
pcmStream.pipe(writer);
|
||||||
|
writer.on("data", (chunk) => out.write(chunk));
|
||||||
|
writer.on("end", () => out.end());
|
||||||
|
writer.on("error", () => out.end());
|
||||||
|
return Readable.toWeb(out) as unknown as globalThis.ReadableStream;
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { VersionCheck } from "@/app/client";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { sse } from "@/lib/db/sse-endpoints";
|
||||||
|
|
||||||
|
type DonateData = {
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
amount: number;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const [current, setCurrent] = useState<DonateData>();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const sfx = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
|
async function ping(data: DonateData) {
|
||||||
|
setCurrent(data);
|
||||||
|
const { name, amount, message } = data;
|
||||||
|
console.log("Requesting TTS");
|
||||||
|
const tts = new Audio(
|
||||||
|
`/api/tts?message=${encodeURIComponent(`${name} โดเนทมา ${amount} บาท\n${message}`)}`,
|
||||||
|
);
|
||||||
|
tts.load();
|
||||||
|
console.log("Waiting for response");
|
||||||
|
const ttsAvailable = await new Promise(
|
||||||
|
(r) => (
|
||||||
|
(tts.onloadeddata = () => r(true)),
|
||||||
|
(tts.onerror = () => r(false))
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log("Transitioning in");
|
||||||
|
setMounted(true);
|
||||||
|
console.log("Running SFX");
|
||||||
|
if (sfx.current) {
|
||||||
|
const player = sfx.current;
|
||||||
|
player.play();
|
||||||
|
console.log("Waiting for SFX to end");
|
||||||
|
await new Promise((r) => (player.onended = r));
|
||||||
|
}
|
||||||
|
console.log("Playing TTS");
|
||||||
|
tts.play();
|
||||||
|
if (ttsAvailable) await new Promise((r) => (tts.onended = r));
|
||||||
|
console.log("Done");
|
||||||
|
setTimeout(() => setMounted(false), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, amount, message, image } = current ?? {};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pendingHeartbeat: Record<number, (v?: unknown) => void> = {};
|
||||||
|
const { clean } = sse.donate.subMany({
|
||||||
|
heartbeat: (tag) => pendingHeartbeat[tag]?.(),
|
||||||
|
ping,
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
const tag = Math.floor(Math.random() * 1000);
|
||||||
|
const promise = new Promise(
|
||||||
|
(r, j) => ((pendingHeartbeat[tag] = r), setTimeout(j, 30000)),
|
||||||
|
);
|
||||||
|
await fetch(`/api/donate/hb?tag=${tag}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
}).catch(() => {});
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
console.log("Heartbeat OK");
|
||||||
|
} catch {
|
||||||
|
console.error("Heartbeat timed out");
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
return clean;
|
||||||
|
// ping({
|
||||||
|
// image: TestImage as unknown as string,
|
||||||
|
// name: "Gayshin",
|
||||||
|
// amount: 1,
|
||||||
|
// message: `สวัสดีครับ วันนี้เรากำลังทดสอบระบบ Text to Speech แบบ mixed language โดยประโยคนี้จะสลับระหว่างภาษาไทยและ English หลายครั้ง เพื่อดูว่า model สามารถ handle pronunciation, pacing, และ sentence transition ได้ดีแค่ไหน ตัวอย่างเช่น "The quick brown fox jumps over the lazy dog" แล้วกลับมาเป็นภาษาไทยอีกครั้ง จากนั้นปิดท้ายด้วยคำว่า thank you for listening และขอให้มีวันที่ดีครับ`,
|
||||||
|
// // message: "ฮีตดฮีตดฮีตดฮีตดฮีตดฮีตด",
|
||||||
|
// });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatePresence>
|
||||||
|
{mounted && (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="relative w-162.5 overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="absolute left-1/2 top-1/2 z-10 -translate-1/2"
|
||||||
|
initial={{ top: "200%", rotateZ: "600deg" }}
|
||||||
|
animate={{ top: "50%", rotateZ: "0deg" }}
|
||||||
|
exit={{ opacity: 0, transition: { delay: 0, duration: 2 } }}
|
||||||
|
transition={{ duration: 1, ease: "circOut" }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="min-w-32"
|
||||||
|
initial={{ width: "1px", padding: 0 }}
|
||||||
|
animate={{ width: "650px", padding: "20px" }}
|
||||||
|
transition={{ delay: 1, duration: 1, ease: "circOut" }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={image ?? ""}
|
||||||
|
alt="User submitted image"
|
||||||
|
className="aspect-square size-32 shrink-0 rounded-2xl bg-black/50"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="rounded-4xl bg-black/50 p-5 flex gap-3 max-w-162.5 h-39 overflow-hidden mx-auto"
|
||||||
|
initial={{ width: 0, padding: 0 }}
|
||||||
|
animate={{ width: "100%", padding: "20px" }}
|
||||||
|
exit={{ opacity: 0, transition: { delay: 0, duration: 2 } }}
|
||||||
|
transition={{ delay: 1, duration: 1, ease: "circOut" }}
|
||||||
|
>
|
||||||
|
<div className="aspect-square size-32 shrink-0 rounded-2xl" />
|
||||||
|
<div className="flex flex-col font-semibold text-3xl min-w-0">
|
||||||
|
<motion.span
|
||||||
|
className="text-[#CB5959] whitespace-nowrap"
|
||||||
|
initial={{ paddingTop: "20px" }}
|
||||||
|
animate={{ paddingTop: 0 }}
|
||||||
|
transition={{ delay: 1, duration: 1, ease: "circOut" }}
|
||||||
|
>
|
||||||
|
{name} :{" "}
|
||||||
|
<span className="text-[#FFCC00]">โดเนทมา {amount}฿</span>
|
||||||
|
</motion.span>
|
||||||
|
<motion.span
|
||||||
|
className={cn(
|
||||||
|
"whitespace-break-spaces wrap-break-word h-20 w-full",
|
||||||
|
(message ?? "").length > 80
|
||||||
|
? "text-xl line-clamp-3"
|
||||||
|
: "text-3xl line-clamp-2",
|
||||||
|
)}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 1.5, duration: 1 }}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</motion.span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<audio src="/assets/donate-sfx.wav" ref={sfx} preload="auto" />
|
||||||
|
<VersionCheck headless />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,10 +8,12 @@
|
|||||||
"@bprogress/next": "^3.2.12",
|
"@bprogress/next": "^3.2.12",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@google/genai": "^2.8.0",
|
||||||
"@headless-tree/core": "^1.6.0",
|
"@headless-tree/core": "^1.6.0",
|
||||||
"@headless-tree/react": "^1.6.0",
|
"@headless-tree/react": "^1.6.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@next/bundle-analyzer": "16.2.6",
|
"@next/bundle-analyzer": "16.2.6",
|
||||||
|
"@origin-space/image-cropper": "^0.1.9",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||||
@@ -53,6 +55,7 @@
|
|||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
"motion": "^12.40.0",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
@@ -72,6 +75,7 @@
|
|||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"uuidv7": "^1.1.0",
|
"uuidv7": "^1.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
|
"wav": "^1.0.2",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.8.2",
|
||||||
"zod": "^4.1.13",
|
"zod": "^4.1.13",
|
||||||
},
|
},
|
||||||
@@ -83,6 +87,7 @@
|
|||||||
"@types/node": "^20.19.26",
|
"@types/node": "^20.19.26",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@types/wav": "^1.0.4",
|
||||||
"drizzle-kit": "^0.31.8",
|
"drizzle-kit": "^0.31.8",
|
||||||
"oxfmt": "^0.52.0",
|
"oxfmt": "^0.52.0",
|
||||||
"oxlint": "^1.67.0",
|
"oxlint": "^1.67.0",
|
||||||
@@ -302,6 +307,8 @@
|
|||||||
|
|
||||||
"@floating-ui/utils": ["@floating-ui/[email protected]", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
"@floating-ui/utils": ["@floating-ui/[email protected]", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||||
|
|
||||||
|
"@google/genai": ["@google/[email protected]", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-pc2ayxqO5+O7AvnHBqpNHIk7PAZkHZgL31tbyx0gJZBSS9qPYiQoqwK7oYOw/ePmG6QY4EMSu+304vD5QlhXAw=="],
|
||||||
|
|
||||||
"@headless-tree/core": ["@headless-tree/[email protected]", "", {}, "sha512-en0EOaZfiCRF2B8DEnhGhSaUf3hVr9Bauye8G8aswPbHOKSyhJiN4bsczz1GvqF4Xb7Ga3LP0vLA6Zih7YLoyw=="],
|
"@headless-tree/core": ["@headless-tree/[email protected]", "", {}, "sha512-en0EOaZfiCRF2B8DEnhGhSaUf3hVr9Bauye8G8aswPbHOKSyhJiN4bsczz1GvqF4Xb7Ga3LP0vLA6Zih7YLoyw=="],
|
||||||
|
|
||||||
"@headless-tree/react": ["@headless-tree/[email protected]", "", { "peerDependencies": { "@headless-tree/core": "*", "react": "*", "react-dom": "*" } }, "sha512-aiRwG6e2EPBSec9uLLy9GlTvAuCtSTouU30Nwcr5ZTsYjG/i7B/ouC8f8Zu4unzo/v1h5ztbemp+EH2TPTKh+g=="],
|
"@headless-tree/react": ["@headless-tree/[email protected]", "", { "peerDependencies": { "@headless-tree/core": "*", "react": "*", "react-dom": "*" } }, "sha512-aiRwG6e2EPBSec9uLLy9GlTvAuCtSTouU30Nwcr5ZTsYjG/i7B/ouC8f8Zu4unzo/v1h5ztbemp+EH2TPTKh+g=="],
|
||||||
@@ -416,6 +423,8 @@
|
|||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/[email protected]", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/[email protected]", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
|
||||||
|
|
||||||
|
"@origin-space/image-cropper": ["@origin-space/[email protected]", "", { "peerDependencies": { "react": "^19.1.0", "react-dom": "^19.1.0" } }, "sha512-Ebl57I6beh7uKjSv8+r3VObEoOXIXhx4SicDUdxa9U7NVY18SzEGfqPDuhNyXuFIB5G/S8nYUToNlIRp9wwB9Q=="],
|
||||||
|
|
||||||
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ=="],
|
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ=="],
|
||||||
|
|
||||||
"@oxfmt/binding-android-arm64": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ=="],
|
"@oxfmt/binding-android-arm64": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ=="],
|
||||||
@@ -728,20 +737,28 @@
|
|||||||
|
|
||||||
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@types/retry": ["@types/[email protected]", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
||||||
|
|
||||||
"@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
"@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
|
"@types/wav": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-s8C6tD2lPnkNkX2rWl0FMSpOXrKb3zB9eRO36p2bViSVw9lVJLSJRK7sOe7lOQ13YpM22/K91o/stMempoWPFw=="],
|
||||||
|
|
||||||
"@xobotyi/scrollbar-width": ["@xobotyi/[email protected]", "", {}, "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="],
|
"@xobotyi/scrollbar-width": ["@xobotyi/[email protected]", "", {}, "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="],
|
||||||
|
|
||||||
"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"acorn-walk": ["[email protected]", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
|
"acorn-walk": ["[email protected]", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
|
||||||
|
|
||||||
|
"agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
"aria-hidden": ["[email protected]", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
"aria-hidden": ["[email protected]", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||||
|
|
||||||
"auto-text-size": ["[email protected]", "", { "peerDependencies": { "react": "*" } }, "sha512-OycpKD8n5jxnNaOiAJoNr3BkBsxRzHHhFQk15rgsOYq+O9lsMSPLbVN7ZibxjTwT7bl6Y5jawOtZJxRWaoxdcQ=="],
|
"auto-text-size": ["[email protected]", "", { "peerDependencies": { "react": "*" } }, "sha512-OycpKD8n5jxnNaOiAJoNr3BkBsxRzHHhFQk15rgsOYq+O9lsMSPLbVN7ZibxjTwT7bl6Y5jawOtZJxRWaoxdcQ=="],
|
||||||
|
|
||||||
"babel-plugin-react-compiler": ["[email protected]", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
"babel-plugin-react-compiler": ["[email protected]", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
||||||
|
|
||||||
|
"base64-js": ["[email protected]", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["[email protected]", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA=="],
|
"baseline-browser-mapping": ["[email protected]", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA=="],
|
||||||
|
|
||||||
"better-auth": ["[email protected]", "", { "dependencies": { "@better-auth/core": "1.4.17", "@better-auth/telemetry": "1.4.17", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew=="],
|
"better-auth": ["[email protected]", "", { "dependencies": { "@better-auth/core": "1.4.17", "@better-auth/telemetry": "1.4.17", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew=="],
|
||||||
@@ -750,8 +767,18 @@
|
|||||||
|
|
||||||
"better-sqlite3": ["[email protected]", "", {}, "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA=="],
|
"better-sqlite3": ["[email protected]", "", {}, "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA=="],
|
||||||
|
|
||||||
|
"bignumber.js": ["[email protected]", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||||
|
|
||||||
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A=="],
|
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A=="],
|
||||||
|
|
||||||
|
"buffer-alloc": ["[email protected]", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="],
|
||||||
|
|
||||||
|
"buffer-alloc-unsafe": ["[email protected]", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="],
|
||||||
|
|
||||||
|
"buffer-equal-constant-time": ["[email protected]", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||||
|
|
||||||
|
"buffer-fill": ["[email protected]", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="],
|
||||||
|
|
||||||
"buffer-from": ["[email protected]", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["[email protected]", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||||
@@ -790,6 +817,8 @@
|
|||||||
|
|
||||||
"core-js": ["[email protected]", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="],
|
"core-js": ["[email protected]", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="],
|
||||||
|
|
||||||
|
"core-util-is": ["[email protected]", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||||
|
|
||||||
"css-in-js-utils": ["[email protected]", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="],
|
"css-in-js-utils": ["[email protected]", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="],
|
||||||
|
|
||||||
"css-tree": ["[email protected]", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="],
|
"css-tree": ["[email protected]", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="],
|
||||||
@@ -818,13 +847,15 @@
|
|||||||
|
|
||||||
"d3-timer": ["[email protected]", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
"d3-timer": ["[email protected]", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||||
|
|
||||||
|
"data-uri-to-buffer": ["[email protected]", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||||
|
|
||||||
"date-fns": ["[email protected]", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
"date-fns": ["[email protected]", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||||
|
|
||||||
"date-fns-jalali": ["[email protected]", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
|
"date-fns-jalali": ["[email protected]", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
|
||||||
|
|
||||||
"debounce": ["[email protected]", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="],
|
"debounce": ["[email protected]", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
"decimal.js-light": ["[email protected]", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
"decimal.js-light": ["[email protected]", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||||
|
|
||||||
@@ -854,6 +885,8 @@
|
|||||||
|
|
||||||
"duplexer": ["[email protected]", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
|
"duplexer": ["[email protected]", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
|
||||||
|
|
||||||
|
"ecdsa-sig-formatter": ["[email protected]", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||||
|
|
||||||
"electron-to-chromium": ["[email protected]", "", {}, "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg=="],
|
"electron-to-chromium": ["[email protected]", "", {}, "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg=="],
|
||||||
|
|
||||||
"embla-carousel": ["[email protected]", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
|
"embla-carousel": ["[email protected]", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
|
||||||
@@ -878,6 +911,8 @@
|
|||||||
|
|
||||||
"exsolve": ["[email protected]", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
|
"exsolve": ["[email protected]", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
|
||||||
|
|
||||||
|
"extend": ["[email protected]", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
"fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-equals": ["[email protected]", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
|
"fast-equals": ["[email protected]", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
|
||||||
@@ -886,10 +921,20 @@
|
|||||||
|
|
||||||
"fastest-stable-stringify": ["[email protected]", "", {}, "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q=="],
|
"fastest-stable-stringify": ["[email protected]", "", {}, "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q=="],
|
||||||
|
|
||||||
|
"fetch-blob": ["[email protected]", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||||
|
|
||||||
"fflate": ["[email protected]", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="],
|
"fflate": ["[email protected]", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="],
|
||||||
|
|
||||||
|
"formdata-polyfill": ["[email protected]", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||||
|
|
||||||
|
"framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="],
|
||||||
|
|
||||||
"fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
"fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
|
"gaxios": ["[email protected]", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
|
||||||
|
|
||||||
|
"gcp-metadata": ["[email protected]", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
||||||
|
|
||||||
"gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
"gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
"get-nonce": ["[email protected]", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
"get-nonce": ["[email protected]", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
@@ -898,14 +943,22 @@
|
|||||||
|
|
||||||
"giget": ["[email protected]", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
"giget": ["[email protected]", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||||
|
|
||||||
|
"google-auth-library": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="],
|
||||||
|
|
||||||
|
"google-logging-utils": ["[email protected]", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||||
|
|
||||||
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"gzip-size": ["[email protected]", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
|
"gzip-size": ["[email protected]", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
|
||||||
|
|
||||||
"html-escaper": ["[email protected]", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
|
"html-escaper": ["[email protected]", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
|
||||||
|
|
||||||
|
"https-proxy-agent": ["[email protected]", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
"hyphenate-style-name": ["[email protected]", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="],
|
"hyphenate-style-name": ["[email protected]", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="],
|
||||||
|
|
||||||
|
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
"inline-style-prefixer": ["[email protected]", "", { "dependencies": { "css-in-js-utils": "^3.1.0" } }, "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw=="],
|
"inline-style-prefixer": ["[email protected]", "", { "dependencies": { "css-in-js-utils": "^3.1.0" } }, "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw=="],
|
||||||
|
|
||||||
"input-otp": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
|
"input-otp": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
|
||||||
@@ -920,6 +973,8 @@
|
|||||||
|
|
||||||
"is-wsl": ["[email protected]", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
"is-wsl": ["[email protected]", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||||
|
|
||||||
|
"isarray": ["[email protected]", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
|
||||||
|
|
||||||
"jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="],
|
"jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="],
|
||||||
|
|
||||||
"jose": ["[email protected]", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
|
"jose": ["[email protected]", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
|
||||||
@@ -930,8 +985,14 @@
|
|||||||
|
|
||||||
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json-bigint": ["[email protected]", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||||
|
|
||||||
"json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"jwa": ["[email protected]", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||||
|
|
||||||
|
"jws": ["[email protected]", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||||
|
|
||||||
"kleur": ["[email protected]", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
"kleur": ["[email protected]", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||||
|
|
||||||
"kysely": ["[email protected]", "", {}, "sha512-u/cAuTL4DRIiO2/g4vNGRgklEKNIj5Q3CG7RoUB5DV5SfEC2hMvPxKi0GWPmnzwL2ryIeud2VTcEEmqzTzEPNw=="],
|
"kysely": ["[email protected]", "", {}, "sha512-u/cAuTL4DRIiO2/g4vNGRgklEKNIj5Q3CG7RoUB5DV5SfEC2hMvPxKi0GWPmnzwL2ryIeud2VTcEEmqzTzEPNw=="],
|
||||||
@@ -978,9 +1039,15 @@
|
|||||||
|
|
||||||
"mdn-data": ["[email protected]", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="],
|
"mdn-data": ["[email protected]", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="],
|
||||||
|
|
||||||
|
"motion": ["[email protected]", "", { "dependencies": { "framer-motion": "^12.40.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA=="],
|
||||||
|
|
||||||
|
"motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
|
||||||
|
|
||||||
|
"motion-utils": ["[email protected]", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
||||||
|
|
||||||
"mrmime": ["[email protected]", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
"mrmime": ["[email protected]", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"nano-css": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "css-tree": "^1.1.2", "csstype": "^3.1.2", "fastest-stable-stringify": "^2.0.2", "inline-style-prefixer": "^7.0.1", "rtl-css-js": "^1.16.1", "stacktrace-js": "^2.0.2", "stylis": "^4.3.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw=="],
|
"nano-css": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "css-tree": "^1.1.2", "csstype": "^3.1.2", "fastest-stable-stringify": "^2.0.2", "inline-style-prefixer": "^7.0.1", "rtl-css-js": "^1.16.1", "stacktrace-js": "^2.0.2", "stylis": "^4.3.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw=="],
|
||||||
|
|
||||||
@@ -992,6 +1059,10 @@
|
|||||||
|
|
||||||
"next-themes": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
"next-themes": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
|
|
||||||
|
"node-domexception": ["[email protected]", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||||
|
|
||||||
|
"node-fetch": ["[email protected]", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||||
|
|
||||||
"node-fetch-native": ["[email protected]", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
"node-fetch-native": ["[email protected]", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
"node-releases": ["[email protected]", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="],
|
"node-releases": ["[email protected]", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="],
|
||||||
@@ -1010,6 +1081,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": ["[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=="],
|
||||||
|
|
||||||
|
"p-retry": ["[email protected]", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||||
|
|
||||||
"pathe": ["[email protected]", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"pathe": ["[email protected]", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
"perfect-debounce": ["[email protected]", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
|
"perfect-debounce": ["[email protected]", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
|
||||||
@@ -1096,6 +1169,8 @@
|
|||||||
|
|
||||||
"react-use": ["[email protected]", "", { "dependencies": { "@types/js-cookie": "^2.2.6", "@xobotyi/scrollbar-width": "^1.9.5", "copy-to-clipboard": "^3.3.1", "fast-deep-equal": "^3.1.3", "fast-shallow-equal": "^1.0.0", "js-cookie": "^2.2.1", "nano-css": "^5.6.2", "react-universal-interface": "^0.6.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.1.0", "set-harmonic-interval": "^1.0.1", "throttle-debounce": "^3.0.1", "ts-easing": "^0.2.0", "tslib": "^2.1.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g=="],
|
"react-use": ["[email protected]", "", { "dependencies": { "@types/js-cookie": "^2.2.6", "@xobotyi/scrollbar-width": "^1.9.5", "copy-to-clipboard": "^3.3.1", "fast-deep-equal": "^3.1.3", "fast-shallow-equal": "^1.0.0", "js-cookie": "^2.2.1", "nano-css": "^5.6.2", "react-universal-interface": "^0.6.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.1.0", "set-harmonic-interval": "^1.0.1", "throttle-debounce": "^3.0.1", "ts-easing": "^0.2.0", "tslib": "^2.1.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g=="],
|
||||||
|
|
||||||
|
"readable-stream": ["[email protected]", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
|
||||||
|
|
||||||
"readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
"recharts": ["[email protected]", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
|
"recharts": ["[email protected]", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
|
||||||
@@ -1110,12 +1185,16 @@
|
|||||||
|
|
||||||
"resolve-pkg-maps": ["[email protected]", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["[email protected]", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"retry": ["[email protected]", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||||
|
|
||||||
"rou3": ["[email protected]", "", {}, "sha512-ELguG3ENDw5NKNmWHO3OGEjcgdxkCNvnMR22gKHEgRXuwiriap5RIYdummOaOiqUNcC5yU5txGCHWNm7KlHuAA=="],
|
"rou3": ["[email protected]", "", {}, "sha512-ELguG3ENDw5NKNmWHO3OGEjcgdxkCNvnMR22gKHEgRXuwiriap5RIYdummOaOiqUNcC5yU5txGCHWNm7KlHuAA=="],
|
||||||
|
|
||||||
"rtl-css-js": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.1.2" } }, "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg=="],
|
"rtl-css-js": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.1.2" } }, "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg=="],
|
||||||
|
|
||||||
"run-applescript": ["[email protected]", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
"run-applescript": ["[email protected]", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
||||||
|
|
||||||
|
"safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"screenfull": ["[email protected]", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="],
|
"screenfull": ["[email protected]", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="],
|
||||||
@@ -1150,6 +1229,10 @@
|
|||||||
|
|
||||||
"stacktrace-js": ["[email protected]", "", { "dependencies": { "error-stack-parser": "^2.0.6", "stack-generator": "^2.0.5", "stacktrace-gps": "^3.0.4" } }, "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg=="],
|
"stacktrace-js": ["[email protected]", "", { "dependencies": { "error-stack-parser": "^2.0.6", "stack-generator": "^2.0.5", "stacktrace-gps": "^3.0.4" } }, "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg=="],
|
||||||
|
|
||||||
|
"stream-parser": ["[email protected]", "", { "dependencies": { "debug": "2" } }, "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ=="],
|
||||||
|
|
||||||
|
"string_decoder": ["[email protected]", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
|
||||||
|
|
||||||
"styled-jsx": ["[email protected]", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
"styled-jsx": ["[email protected]", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||||
|
|
||||||
"stylis": ["[email protected]", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
|
"stylis": ["[email protected]", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
|
||||||
@@ -1198,11 +1281,15 @@
|
|||||||
|
|
||||||
"victory-vendor": ["[email protected]", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
|
"victory-vendor": ["[email protected]", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
|
||||||
|
|
||||||
|
"wav": ["[email protected]", "", { "dependencies": { "buffer-alloc": "^1.1.0", "buffer-from": "^1.0.0", "debug": "^2.2.0", "readable-stream": "^1.1.14", "stream-parser": "^0.3.1" } }, "sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg=="],
|
||||||
|
|
||||||
|
"web-streams-polyfill": ["[email protected]", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||||
|
|
||||||
"web-vitals": ["[email protected]", "", {}, "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA=="],
|
"web-vitals": ["[email protected]", "", {}, "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA=="],
|
||||||
|
|
||||||
"webpack-bundle-analyzer": ["[email protected]", "", { "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^7.2.0", "debounce": "^1.2.1", "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", "html-escaper": "^2.0.2", "is-plain-object": "^5.0.0", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ=="],
|
"webpack-bundle-analyzer": ["[email protected]", "", { "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^7.2.0", "debounce": "^1.2.1", "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", "html-escaper": "^2.0.2", "is-plain-object": "^5.0.0", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ=="],
|
||||||
|
|
||||||
"ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||||
|
|
||||||
"wsl-utils": ["[email protected]", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
"wsl-utils": ["[email protected]", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||||
|
|
||||||
@@ -1218,12 +1305,16 @@
|
|||||||
|
|
||||||
"zod": ["[email protected]", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
"zod": ["[email protected]", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"@babel/core/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@babel/core/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/core/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/helper-compilation-targets/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@babel/helper-create-class-features-plugin/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/helper-create-class-features-plugin/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"@babel/traverse/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@better-auth/cli/drizzle-orm": ["[email protected]", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="],
|
"@better-auth/cli/drizzle-orm": ["[email protected]", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
"@esbuild-kit/core-utils/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||||
@@ -1294,6 +1385,10 @@
|
|||||||
|
|
||||||
"browserslist/caniuse-lite": ["[email protected]", "", {}, "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ=="],
|
"browserslist/caniuse-lite": ["[email protected]", "", {}, "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ=="],
|
||||||
|
|
||||||
|
"esbuild-register/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"https-proxy-agent/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"lightningcss/detect-libc": ["[email protected]", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
|
"lightningcss/detect-libc": ["[email protected]", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
|
||||||
|
|
||||||
"nano-css/csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"nano-css/csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
@@ -1322,6 +1417,12 @@
|
|||||||
|
|
||||||
"webpack-bundle-analyzer/commander": ["[email protected]", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
"webpack-bundle-analyzer/commander": ["[email protected]", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||||
|
|
||||||
|
"webpack-bundle-analyzer/ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||||
|
|
||||||
|
"@babel/core/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"@babel/traverse/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||||
@@ -1365,5 +1466,9 @@
|
|||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||||
|
|
||||||
|
"esbuild-register/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"https-proxy-agent/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
// oxlint-disable next/no-img-element
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
PlusIcon,
|
||||||
|
XIcon,
|
||||||
|
ZoomInIcon,
|
||||||
|
ZoomOutIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { useFileUpload } from "@/hooks/use-file-upload";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Cropper,
|
||||||
|
CropperCropArea,
|
||||||
|
CropperDescription,
|
||||||
|
CropperImage,
|
||||||
|
} from "@/components/ui/cropper";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
|
||||||
|
// Define type for pixel crop area
|
||||||
|
type Area = { x: number; y: number; width: number; height: number };
|
||||||
|
|
||||||
|
// Helper function to create a cropped image blob
|
||||||
|
const createImage = (url: string): Promise<HTMLImageElement> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.addEventListener("load", () => resolve(image));
|
||||||
|
image.addEventListener("error", (error) => reject(error));
|
||||||
|
image.setAttribute("crossOrigin", "anonymous"); // Needed for canvas Tainted check
|
||||||
|
image.src = url;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getCroppedImg(
|
||||||
|
imageSrc: string,
|
||||||
|
pixelCrop: Area,
|
||||||
|
outputWidth: number = pixelCrop.width, // Optional: specify output size
|
||||||
|
outputHeight: number = pixelCrop.height,
|
||||||
|
): Promise<Blob | null> {
|
||||||
|
try {
|
||||||
|
const image = await createImage(imageSrc);
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set canvas size to desired output size
|
||||||
|
canvas.width = outputWidth;
|
||||||
|
canvas.height = outputHeight;
|
||||||
|
|
||||||
|
// Draw the cropped image onto the canvas
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
pixelCrop.x,
|
||||||
|
pixelCrop.y,
|
||||||
|
pixelCrop.width,
|
||||||
|
pixelCrop.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
outputWidth, // Draw onto the output size
|
||||||
|
outputHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert canvas to blob
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
resolve(blob);
|
||||||
|
}, "image/jpeg"); // Specify format and quality if needed
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in getCroppedImg:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
const [
|
||||||
|
{ files, isDragging },
|
||||||
|
{
|
||||||
|
handleDragEnter,
|
||||||
|
handleDragLeave,
|
||||||
|
handleDragOver,
|
||||||
|
handleDrop,
|
||||||
|
openFileDialog,
|
||||||
|
removeFile,
|
||||||
|
getInputProps,
|
||||||
|
},
|
||||||
|
] = useFileUpload({
|
||||||
|
accept: "image/*",
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewUrl = files[0]?.preview || null;
|
||||||
|
const fileId = files[0]?.id;
|
||||||
|
|
||||||
|
const [finalImageUrl, setFinalImageUrl] = useState<string | null>(null);
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// Ref to track the previous file ID to detect new uploads
|
||||||
|
const previousFileIdRef = useRef<string | undefined | null>(null);
|
||||||
|
|
||||||
|
// State to store the desired crop area in pixels
|
||||||
|
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
|
||||||
|
|
||||||
|
// State for zoom level
|
||||||
|
const [zoom, setZoom] = useState(1);
|
||||||
|
|
||||||
|
// Callback for Cropper to provide crop data - Wrap with useCallback
|
||||||
|
const handleCropChange = useCallback((pixels: Area | null) => {
|
||||||
|
setCroppedAreaPixels(pixels);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleApply = async () => {
|
||||||
|
// Check if we have the necessary data
|
||||||
|
if (!previewUrl || !fileId || !croppedAreaPixels) {
|
||||||
|
console.error("Missing data for apply:", {
|
||||||
|
croppedAreaPixels,
|
||||||
|
fileId,
|
||||||
|
previewUrl,
|
||||||
|
});
|
||||||
|
// Remove file if apply is clicked without crop data?
|
||||||
|
if (fileId) {
|
||||||
|
removeFile(fileId);
|
||||||
|
setCroppedAreaPixels(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Get the cropped image blob using the helper
|
||||||
|
const croppedBlob = await getCroppedImg(previewUrl, croppedAreaPixels);
|
||||||
|
|
||||||
|
if (!croppedBlob) {
|
||||||
|
throw new Error("Failed to generate cropped image blob.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create a NEW object URL from the cropped blob
|
||||||
|
const newFinalUrl = URL.createObjectURL(croppedBlob);
|
||||||
|
|
||||||
|
// 3. Revoke the OLD finalImageUrl if it exists
|
||||||
|
if (finalImageUrl) {
|
||||||
|
URL.revokeObjectURL(finalImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Set the final avatar state to the NEW URL
|
||||||
|
setFinalImageUrl(newFinalUrl);
|
||||||
|
|
||||||
|
// 5. Close the dialog (don't remove the file yet)
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during apply:", error);
|
||||||
|
// Close the dialog even if cropping fails
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFinalImage = () => {
|
||||||
|
if (finalImageUrl) {
|
||||||
|
URL.revokeObjectURL(finalImageUrl);
|
||||||
|
}
|
||||||
|
setFinalImageUrl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentFinalUrl = finalImageUrl;
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
if (currentFinalUrl?.startsWith("blob:")) {
|
||||||
|
URL.revokeObjectURL(currentFinalUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [finalImageUrl]);
|
||||||
|
|
||||||
|
// Effect to open dialog when a *new* file is ready
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if fileId exists and is different from the previous one
|
||||||
|
if (fileId && fileId !== previousFileIdRef.current) {
|
||||||
|
setIsDialogOpen(true); // Open dialog for the new file
|
||||||
|
setCroppedAreaPixels(null); // Reset crop area for the new file
|
||||||
|
setZoom(1); // Reset zoom for the new file
|
||||||
|
}
|
||||||
|
// Update the ref to the current fileId for the next render
|
||||||
|
previousFileIdRef.current = fileId;
|
||||||
|
}, [fileId]); // Depend only on fileId
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative inline-flex">
|
||||||
|
{/* Drop area - uses finalImageUrl */}
|
||||||
|
<button
|
||||||
|
aria-label={finalImageUrl ? "Change image" : "Upload image"}
|
||||||
|
className="relative flex size-32 items-center justify-center overflow-hidden rounded-lg border border-input border-dashed outline-none transition-colors hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-disabled:pointer-events-none has-[img]:border-none has-disabled:opacity-50 data-[dragging=true]:bg-accent/50"
|
||||||
|
data-dragging={isDragging || undefined}
|
||||||
|
onClick={openFileDialog}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{finalImageUrl ? (
|
||||||
|
<img
|
||||||
|
alt="User avatar"
|
||||||
|
className="size-full object-cover"
|
||||||
|
height={128}
|
||||||
|
src={finalImageUrl}
|
||||||
|
style={{ objectFit: "cover" }}
|
||||||
|
width={128}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div aria-hidden="true">
|
||||||
|
<PlusIcon className="size-8 opacity-60" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{/* Remove button - depends on finalImageUrl */}
|
||||||
|
{finalImageUrl && (
|
||||||
|
<Button
|
||||||
|
aria-label="Remove image"
|
||||||
|
className="-top-1 -right-1 absolute size-6 rounded-full border-2 border-background shadow-none focus-visible:border-background"
|
||||||
|
onClick={handleRemoveFinalImage}
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<XIcon className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
{...getInputProps()}
|
||||||
|
aria-label="Upload image file"
|
||||||
|
className="sr-only"
|
||||||
|
tabIndex={-1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cropper Dialog - Use isDialogOpen for open prop */}
|
||||||
|
<Dialog onOpenChange={setIsDialogOpen} open={isDialogOpen}>
|
||||||
|
<DialogContent className="gap-0 p-0 sm:max-w-140 *:[button]:hidden">
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
Crop image dialog
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogHeader className="contents space-y-0 text-left">
|
||||||
|
<DialogTitle className="flex items-center justify-between border-b p-4 text-base">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
aria-label="Cancel"
|
||||||
|
className="-my-1 opacity-60"
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
<span>Crop image</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
autoFocus
|
||||||
|
className="-my-1"
|
||||||
|
disabled={!previewUrl}
|
||||||
|
onClick={handleApply}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{previewUrl && (
|
||||||
|
<Cropper
|
||||||
|
className="h-96 sm:h-120"
|
||||||
|
image={previewUrl}
|
||||||
|
onCropChange={handleCropChange}
|
||||||
|
onZoomChange={setZoom}
|
||||||
|
zoom={zoom}
|
||||||
|
>
|
||||||
|
<CropperDescription />
|
||||||
|
<CropperImage />
|
||||||
|
<CropperCropArea />
|
||||||
|
</Cropper>
|
||||||
|
)}
|
||||||
|
<DialogFooter className="border-t px-4 py-6">
|
||||||
|
<div className="mx-auto flex w-full max-w-80 items-center gap-4">
|
||||||
|
<ZoomOutIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="shrink-0 opacity-60"
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
aria-label="Zoom slider"
|
||||||
|
defaultValue={[1]}
|
||||||
|
max={3}
|
||||||
|
min={1}
|
||||||
|
onValueChange={(value) => setZoom(value[0])}
|
||||||
|
step={0.1}
|
||||||
|
value={[zoom]}
|
||||||
|
/>
|
||||||
|
<ZoomInIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="shrink-0 opacity-60"
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
export type FormSubmitResult =
|
||||||
|
| {
|
||||||
|
toast?: string;
|
||||||
|
close?: boolean;
|
||||||
|
error?:
|
||||||
|
| string
|
||||||
|
| { what: string; where: string }
|
||||||
|
| { what: string; where: string }[];
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
export function defineFormHandler<TSchema extends z.ZodType>(
|
||||||
|
schema: TSchema,
|
||||||
|
action: (input: z.output<TSchema>) => Promise<FormSubmitResult>,
|
||||||
|
): (form: FormData) => Promise<FormSubmitResult>;
|
||||||
|
export function defineFormHandler<
|
||||||
|
TAction extends (form: FormData) => Promise<FormSubmitResult>,
|
||||||
|
>(action: TAction): TAction;
|
||||||
|
export function defineFormHandler(
|
||||||
|
schemaOrAction: z.ZodType | ((form: FormData) => Promise<FormSubmitResult>),
|
||||||
|
action?: (input: unknown) => Promise<FormSubmitResult>,
|
||||||
|
): (form: FormData) => Promise<FormSubmitResult> {
|
||||||
|
if (!action)
|
||||||
|
return schemaOrAction as (form: FormData) => Promise<FormSubmitResult>;
|
||||||
|
const schema = schemaOrAction as z.ZodType;
|
||||||
|
return async (form: FormData) => {
|
||||||
|
// "use server";
|
||||||
|
const raw = Object.fromEntries([...form.entries()]);
|
||||||
|
const parsed = schema.safeParse(raw);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return {
|
||||||
|
error: parsed.error.issues.map((issue) => ({
|
||||||
|
what: issue.message,
|
||||||
|
where: issue.path.join("."),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return action(parsed.data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formParse<TSchema extends z.ZodType>(
|
||||||
|
schema: TSchema,
|
||||||
|
data: FormData,
|
||||||
|
): { $: z.output<TSchema>; error: { what: string; where: string }[] | string } {
|
||||||
|
const raw = Object.fromEntries([...data.entries()]);
|
||||||
|
const parsed = schema.safeParse(raw);
|
||||||
|
if (!parsed.success) {
|
||||||
|
if (
|
||||||
|
parsed.error.issues.length === 1 &&
|
||||||
|
parsed.error.issues[0].path.length === 0
|
||||||
|
)
|
||||||
|
return { error: parsed.error.issues[0].message, $: undefined as any };
|
||||||
|
return {
|
||||||
|
error: parsed.error.issues.map((issue) => ({
|
||||||
|
what: issue.message,
|
||||||
|
where: issue.path.join("."),
|
||||||
|
})),
|
||||||
|
$: undefined as any,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { $: parsed.data, error: undefined as any };
|
||||||
|
}
|
||||||
+120
-43
@@ -2,14 +2,12 @@
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type {
|
|
||||||
TypedFormData,
|
|
||||||
TypedFormDataShape,
|
|
||||||
} from "@/app/(ui)/rubgram/type";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { DialogClose } from "./ui/dialog";
|
import { DialogClose } from "./ui/dialog";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
||||||
import { Divide } from "lucide-react";
|
import { Divide } from "lucide-react";
|
||||||
|
import type { FormSubmitResult } from "./form-submit";
|
||||||
|
|
||||||
const AUTOSAVE_TTL_MS = 10 * 60 * 1000;
|
const AUTOSAVE_TTL_MS = 10 * 60 * 1000;
|
||||||
const STORAGE_PREFIX = "form:autosave:";
|
const STORAGE_PREFIX = "form:autosave:";
|
||||||
@@ -19,12 +17,14 @@ type FormValues = Record<string, unknown>;
|
|||||||
type FormContextValue = {
|
type FormContextValue = {
|
||||||
id: string;
|
id: string;
|
||||||
values: FormValues;
|
values: FormValues;
|
||||||
|
errors: Record<string, string>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
setValue: (name: string, value: unknown) => void;
|
setValue: (name: string, value: unknown) => void;
|
||||||
updateValues: (updater: (prev: FormValues) => FormValues) => void;
|
updateValues: (updater: (prev: FormValues) => FormValues) => void;
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
submit: () => Promise<void>;
|
submit: () => Promise<void>;
|
||||||
closeDialog: () => void;
|
closeDialog: () => void;
|
||||||
|
setErrors: React.Dispatch<React.SetStateAction<Record<string, string>>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormContext = React.createContext<FormContextValue | null>(null);
|
const FormContext = React.createContext<FormContextValue | null>(null);
|
||||||
@@ -87,19 +87,6 @@ function appendFormValue(form: FormData, key: string, value: unknown) {
|
|||||||
form.append(key, typeof value === "string" ? value : String(value));
|
form.append(key, typeof value === "string" ? value : String(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toTypedFormData<T extends TypedFormDataShape>(
|
|
||||||
formData: FormData,
|
|
||||||
): TypedFormData<T> {
|
|
||||||
const base = formData as unknown as TypedFormData<T>;
|
|
||||||
if (!("raw" in base)) {
|
|
||||||
Object.defineProperty(base, "raw", {
|
|
||||||
value: formData,
|
|
||||||
enumerable: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInputValue(eventOrValue: unknown) {
|
function getInputValue(eventOrValue: unknown) {
|
||||||
const event = eventOrValue as React.ChangeEvent<HTMLInputElement>;
|
const event = eventOrValue as React.ChangeEvent<HTMLInputElement>;
|
||||||
const target = event?.target as
|
const target = event?.target as
|
||||||
@@ -118,35 +105,25 @@ function getInputValue(eventOrValue: unknown) {
|
|||||||
return eventOrValue;
|
return eventOrValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubmitFnResult =
|
type FormProviderProps = Omit<React.ComponentProps<"form">, "id" | "onSubmit"> & {
|
||||||
| {
|
|
||||||
toast?: string;
|
|
||||||
close?: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
type FormProviderProps<T extends TypedFormDataShape> = Omit<
|
|
||||||
React.ComponentProps<"form">,
|
|
||||||
"id" | "onSubmit"
|
|
||||||
> & {
|
|
||||||
// Unique Autosave Key
|
// Unique Autosave Key
|
||||||
id: string;
|
id: string;
|
||||||
onSubmit?: (
|
onSubmit?: (form: FormData) => FormSubmitResult | Promise<FormSubmitResult>;
|
||||||
form: TypedFormData<T>,
|
|
||||||
) => SubmitFnResult | Promise<SubmitFnResult>;
|
|
||||||
values?: FormValues;
|
values?: FormValues;
|
||||||
|
inDialog?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FormProvider<T extends TypedFormDataShape>({
|
export function FormProvider({
|
||||||
id,
|
id,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
inDialog = true,
|
||||||
values: defaultValues = {},
|
values: defaultValues = {},
|
||||||
...props
|
...props
|
||||||
}: FormProviderProps<T>) {
|
}: FormProviderProps) {
|
||||||
const [values, setValues] = React.useState<FormValues>(defaultValues);
|
const [values, setValues] = React.useState<FormValues>(defaultValues);
|
||||||
|
const [errors, setErrors] = React.useState<Record<string, string>>({});
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const formRef = React.useRef<HTMLFormElement | null>(null);
|
const formRef = React.useRef<HTMLFormElement | null>(null);
|
||||||
const [storageLock, setStorageLock] = React.useState<"r" | "w" | null>("r");
|
const [storageLock, setStorageLock] = React.useState<"r" | "w" | null>("r");
|
||||||
@@ -180,6 +157,12 @@ export function FormProvider<T extends TypedFormDataShape>({
|
|||||||
|
|
||||||
const setValue = React.useCallback((name: string, value: unknown) => {
|
const setValue = React.useCallback((name: string, value: unknown) => {
|
||||||
setValues((prev) => ({ ...prev, [name]: value }));
|
setValues((prev) => ({ ...prev, [name]: value }));
|
||||||
|
setErrors((prev) => {
|
||||||
|
if (!prev[name]) return prev;
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[name];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
const updateValues = React.useCallback(
|
const updateValues = React.useCallback(
|
||||||
(updater: (prev: FormValues) => FormValues) => {
|
(updater: (prev: FormValues) => FormValues) => {
|
||||||
@@ -190,6 +173,7 @@ export function FormProvider<T extends TypedFormDataShape>({
|
|||||||
|
|
||||||
const clear = React.useCallback(() => {
|
const clear = React.useCallback(() => {
|
||||||
setValues({});
|
setValues({});
|
||||||
|
setErrors({});
|
||||||
localStorage.removeItem(storageKey(id));
|
localStorage.removeItem(storageKey(id));
|
||||||
formRef.current?.reset();
|
formRef.current?.reset();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
@@ -204,12 +188,25 @@ export function FormProvider<T extends TypedFormDataShape>({
|
|||||||
appendFormValue(formData, key, value);
|
appendFormValue(formData, key, value);
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setErrors({});
|
||||||
try {
|
try {
|
||||||
const res = await onSubmit?.(toTypedFormData(formData));
|
const res = await onSubmit?.(formData);
|
||||||
if (res && typeof res === "object") {
|
if (res && typeof res === "object") {
|
||||||
if ("error" in res) {
|
if ("error" in res) {
|
||||||
const { error } = res;
|
const { error } = res;
|
||||||
if (typeof error === "string") throw new Error(error);
|
if (typeof error === "string") {
|
||||||
|
toast.error(error);
|
||||||
|
} else if (Array.isArray(error)) {
|
||||||
|
const fieldErrors: Record<string, string> = {};
|
||||||
|
for (const e of error) {
|
||||||
|
fieldErrors[e.where] = e.what;
|
||||||
|
}
|
||||||
|
setErrors(fieldErrors);
|
||||||
|
} else if (error) {
|
||||||
|
setErrors({ [error.where]: error.what });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setErrors({});
|
||||||
}
|
}
|
||||||
if ("toast" in res) {
|
if ("toast" in res) {
|
||||||
const { toast: toastMsg } = res;
|
const { toast: toastMsg } = res;
|
||||||
@@ -239,14 +236,16 @@ export function FormProvider<T extends TypedFormDataShape>({
|
|||||||
() => ({
|
() => ({
|
||||||
id,
|
id,
|
||||||
values,
|
values,
|
||||||
|
errors,
|
||||||
loading,
|
loading,
|
||||||
setValue,
|
setValue,
|
||||||
updateValues,
|
updateValues,
|
||||||
clear,
|
clear,
|
||||||
submit,
|
submit,
|
||||||
closeDialog,
|
closeDialog,
|
||||||
|
setErrors,
|
||||||
}),
|
}),
|
||||||
[id, values, loading, setValue, updateValues, clear, submit, closeDialog],
|
[id, values, errors, loading, setValue, updateValues, clear, submit, closeDialog, setErrors],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -263,7 +262,7 @@ export function FormProvider<T extends TypedFormDataShape>({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</form>
|
</form>
|
||||||
<DialogClose ref={closerRef} />
|
{inDialog && <DialogClose ref={closerRef} />}
|
||||||
</FormContext.Provider>
|
</FormContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -286,7 +285,7 @@ export function FormInput({
|
|||||||
subLabel,
|
subLabel,
|
||||||
className,
|
className,
|
||||||
}: FormInputProps) {
|
}: FormInputProps) {
|
||||||
const { values, setValue, updateValues } = useFormContext();
|
const { values, setValue, updateValues, errors } = useFormContext();
|
||||||
const childProps = (children.props ?? {}) as Record<string, unknown>;
|
const childProps = (children.props ?? {}) as Record<string, unknown>;
|
||||||
const value = values[name] ?? childProps?.defaultValue ?? "";
|
const value = values[name] ?? childProps?.defaultValue ?? "";
|
||||||
const nextProps: Record<string, unknown> = { name, defaultValue: undefined };
|
const nextProps: Record<string, unknown> = { name, defaultValue: undefined };
|
||||||
@@ -356,9 +355,17 @@ export function FormInput({
|
|||||||
)}
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
{React.cloneElement(children || Divide, nextProps)}
|
{React.cloneElement(children || Divide, nextProps)}
|
||||||
|
{errors[name] && (
|
||||||
|
<span className="text-destructive text-sm">{errors[name]}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
React.cloneElement(children || Divide, nextProps)
|
<div className={cn(className)}>
|
||||||
|
{React.cloneElement(children || Divide, nextProps)}
|
||||||
|
{errors[name] && (
|
||||||
|
<span className="text-destructive text-sm">{errors[name]}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -370,7 +377,7 @@ type FormActionProps = Omit<
|
|||||||
> & {
|
> & {
|
||||||
type: "clear" | "submit" | "action";
|
type: "clear" | "submit" | "action";
|
||||||
|
|
||||||
action?: () => SubmitFnResult | Promise<SubmitFnResult>;
|
action?: () => FormSubmitResult | Promise<FormSubmitResult>;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
loading?: React.ReactNode;
|
loading?: React.ReactNode;
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
@@ -384,7 +391,7 @@ export function FormAction({
|
|||||||
loading: loadingComponent,
|
loading: loadingComponent,
|
||||||
...props
|
...props
|
||||||
}: FormActionProps) {
|
}: FormActionProps) {
|
||||||
const { loading: formLoading, clear, closeDialog } = useFormContext();
|
const { loading: formLoading, clear, closeDialog, setErrors } = useFormContext();
|
||||||
const [actionLoading, setActionLoading] = React.useState(false);
|
const [actionLoading, setActionLoading] = React.useState(false);
|
||||||
const loading = formLoading || actionLoading;
|
const loading = formLoading || actionLoading;
|
||||||
|
|
||||||
@@ -401,7 +408,19 @@ export function FormAction({
|
|||||||
if (res && typeof res === "object") {
|
if (res && typeof res === "object") {
|
||||||
if ("error" in res) {
|
if ("error" in res) {
|
||||||
const { error } = res;
|
const { error } = res;
|
||||||
if (typeof error === "string") throw new Error(error);
|
if (typeof error === "string") {
|
||||||
|
toast.error(error);
|
||||||
|
} else if (Array.isArray(error)) {
|
||||||
|
const fieldErrors: Record<string, string> = {};
|
||||||
|
for (const e of error) {
|
||||||
|
fieldErrors[e.where] = e.what;
|
||||||
|
}
|
||||||
|
setErrors(fieldErrors);
|
||||||
|
} else if (error) {
|
||||||
|
setErrors({ [error.where]: error.what });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setErrors({});
|
||||||
}
|
}
|
||||||
if ("toast" in res) {
|
if ("toast" in res) {
|
||||||
const { toast: toastMsg } = res;
|
const { toast: toastMsg } = res;
|
||||||
@@ -452,3 +471,61 @@ export function FormRow({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function FormTab({
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
tabs,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label?: React.ReactNode;
|
||||||
|
name: string;
|
||||||
|
tabs: { label: React.ReactNode; value: string }[];
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { values, setValue } = useFormContext();
|
||||||
|
const active = (values[name] as string | undefined) ?? tabs[0]?.value;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (values[name] === undefined && tabs[0]) {
|
||||||
|
setValue(name, tabs[0].value);
|
||||||
|
}
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: only run on mount to seed default
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex w-full justify-between items-end">
|
||||||
|
{label}
|
||||||
|
<Tabs value={active} onValueChange={(v) => setValue(name, v)}>
|
||||||
|
<TabsList>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabsTrigger key={tab.value} value={tab.value}>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted-foreground/10 p-3 rounded-md">
|
||||||
|
{React.Children.map(children, (child) => {
|
||||||
|
if (!React.isValidElement(child)) return null;
|
||||||
|
const childValue = (child.props as Record<string, unknown>).value;
|
||||||
|
return childValue === active ? child : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormChoice({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
// biome-ignore lint/style/useExportType: not a type
|
||||||
|
value: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Cropper as CropperPrimitive } from "@origin-space/image-cropper";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Cropper({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CropperPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CropperPrimitive.Root
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-move touch-none items-center justify-center overflow-hidden focus:outline-none",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="cropper"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CropperDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CropperPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<CropperPrimitive.Description
|
||||||
|
className={cn("sr-only", className)}
|
||||||
|
data-slot="cropper-description"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CropperImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CropperPrimitive.Image>) {
|
||||||
|
return (
|
||||||
|
<CropperPrimitive.Image
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none h-full w-full object-cover",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="cropper-image"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CropperCropArea({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CropperPrimitive.CropArea>) {
|
||||||
|
return (
|
||||||
|
<CropperPrimitive.CropArea
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute border-3 border-white shadow-[0_0_0_9999px_rgba(0,0,0,0.3)] in-[[data-slot=cropper]:focus-visible]:ring-[3px] in-[[data-slot=cropper]:focus-visible]:ring-white/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="cropper-crop-area"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Cropper, CropperDescription, CropperImage, CropperCropArea };
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ChangeEvent,
|
||||||
|
type DragEvent,
|
||||||
|
type InputHTMLAttributes,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export type FileMetadata = {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
url: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileWithPreview = {
|
||||||
|
file: File | FileMetadata;
|
||||||
|
id: string;
|
||||||
|
preview?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileUploadOptions = {
|
||||||
|
maxFiles?: number; // Only used when multiple is true, defaults to Infinity
|
||||||
|
maxSize?: number; // in bytes
|
||||||
|
accept?: string;
|
||||||
|
multiple?: boolean; // Defaults to false
|
||||||
|
initialFiles?: FileMetadata[];
|
||||||
|
onFilesChange?: (files: FileWithPreview[]) => void; // Callback when files change
|
||||||
|
onFilesAdded?: (addedFiles: FileWithPreview[]) => void; // Callback when new files are added
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileUploadState = {
|
||||||
|
files: FileWithPreview[];
|
||||||
|
isDragging: boolean;
|
||||||
|
errors: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileUploadActions = {
|
||||||
|
addFiles: (files: FileList | File[]) => void;
|
||||||
|
removeFile: (id: string) => void;
|
||||||
|
clearFiles: () => void;
|
||||||
|
clearErrors: () => void;
|
||||||
|
handleDragEnter: (e: DragEvent<HTMLElement>) => void;
|
||||||
|
handleDragLeave: (e: DragEvent<HTMLElement>) => void;
|
||||||
|
handleDragOver: (e: DragEvent<HTMLElement>) => void;
|
||||||
|
handleDrop: (e: DragEvent<HTMLElement>) => void;
|
||||||
|
handleFileChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
openFileDialog: () => void;
|
||||||
|
getInputProps: (
|
||||||
|
props?: InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
) => InputHTMLAttributes<HTMLInputElement> & {
|
||||||
|
// Use `any` here to avoid cross-React ref type conflicts across packages
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: intentional
|
||||||
|
ref: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFileUpload = (
|
||||||
|
options: FileUploadOptions = {},
|
||||||
|
): [FileUploadState, FileUploadActions] => {
|
||||||
|
const {
|
||||||
|
maxFiles = Number.POSITIVE_INFINITY,
|
||||||
|
maxSize = Number.POSITIVE_INFINITY,
|
||||||
|
accept = "*",
|
||||||
|
multiple = false,
|
||||||
|
initialFiles = [],
|
||||||
|
onFilesChange,
|
||||||
|
onFilesAdded,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const [state, setState] = useState<FileUploadState>({
|
||||||
|
errors: [],
|
||||||
|
files: initialFiles.map((file) => ({
|
||||||
|
file,
|
||||||
|
id: file.id,
|
||||||
|
preview: file.url,
|
||||||
|
})),
|
||||||
|
isDragging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const validateFile = useCallback(
|
||||||
|
(file: File | FileMetadata): string | null => {
|
||||||
|
if (file instanceof File) {
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
return `File "${file.name}" exceeds the maximum size of ${formatBytes(maxSize)}.`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
return `File "${file.name}" exceeds the maximum size of ${formatBytes(maxSize)}.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accept !== "*") {
|
||||||
|
const acceptedTypes = accept.split(",").map((type) => type.trim());
|
||||||
|
const fileType = file instanceof File ? file.type || "" : file.type;
|
||||||
|
const fileExtension = `.${file instanceof File ? file.name.split(".").pop() : file.name.split(".").pop()}`;
|
||||||
|
|
||||||
|
const isAccepted = acceptedTypes.some((type) => {
|
||||||
|
if (type.startsWith(".")) {
|
||||||
|
return fileExtension.toLowerCase() === type.toLowerCase();
|
||||||
|
}
|
||||||
|
if (type.endsWith("/*")) {
|
||||||
|
const baseType = type.split("/")[0];
|
||||||
|
return fileType.startsWith(`${baseType}/`);
|
||||||
|
}
|
||||||
|
return fileType === type;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isAccepted) {
|
||||||
|
return `File "${file instanceof File ? file.name : file.name}" is not an accepted file type.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[accept, maxSize],
|
||||||
|
);
|
||||||
|
|
||||||
|
const createPreview = useCallback(
|
||||||
|
(file: File | FileMetadata): string | undefined => {
|
||||||
|
if (file instanceof File) {
|
||||||
|
return URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
return file.url;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateUniqueId = useCallback((file: File | FileMetadata): string => {
|
||||||
|
if (file instanceof File) {
|
||||||
|
return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
}
|
||||||
|
return file.id;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearFiles = useCallback(() => {
|
||||||
|
setState((prev) => {
|
||||||
|
// Clean up object URLs
|
||||||
|
for (const file of prev.files ?? []) {
|
||||||
|
if (
|
||||||
|
file.preview &&
|
||||||
|
file.file instanceof File &&
|
||||||
|
file.file.type.startsWith("image/")
|
||||||
|
) {
|
||||||
|
URL.revokeObjectURL(file.preview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
...prev,
|
||||||
|
errors: [],
|
||||||
|
files: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
onFilesChange?.(newState.files);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}, [onFilesChange]);
|
||||||
|
|
||||||
|
const addFiles = useCallback(
|
||||||
|
(newFiles: FileList | File[]) => {
|
||||||
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
|
|
||||||
|
const newFilesArray = Array.from(newFiles);
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// Clear existing errors when new files are uploaded
|
||||||
|
setState((prev) => ({ ...prev, errors: [] }));
|
||||||
|
|
||||||
|
// In single file mode, clear existing files first
|
||||||
|
if (!multiple) {
|
||||||
|
clearFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if adding these files would exceed maxFiles (only in multiple mode)
|
||||||
|
if (
|
||||||
|
multiple &&
|
||||||
|
maxFiles !== Number.POSITIVE_INFINITY &&
|
||||||
|
state.files.length + newFilesArray.length > maxFiles
|
||||||
|
) {
|
||||||
|
errors.push(`You can only upload a maximum of ${maxFiles} files.`);
|
||||||
|
setState((prev) => ({ ...prev, errors }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validFiles: FileWithPreview[] = [];
|
||||||
|
|
||||||
|
for (const file of newFilesArray) {
|
||||||
|
if (multiple) {
|
||||||
|
const isDuplicate = state.files.some(
|
||||||
|
(existingFile) =>
|
||||||
|
existingFile.file.name === file.name &&
|
||||||
|
existingFile.file.size === file.size,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
errors.push(
|
||||||
|
multiple
|
||||||
|
? `Some files exceed the maximum size of ${formatBytes(maxSize)}.`
|
||||||
|
: `File exceeds the maximum size of ${formatBytes(maxSize)}.`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = validateFile(file);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
errors.push(error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validFiles.push({
|
||||||
|
file,
|
||||||
|
id: generateUniqueId(file),
|
||||||
|
preview: createPreview(file),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update state if we have valid files to add
|
||||||
|
if (validFiles.length > 0) {
|
||||||
|
// Call the onFilesAdded callback with the newly added valid files
|
||||||
|
onFilesAdded?.(validFiles);
|
||||||
|
|
||||||
|
setState((prev) => {
|
||||||
|
const newFiles = !multiple
|
||||||
|
? validFiles
|
||||||
|
: [...prev.files, ...validFiles];
|
||||||
|
onFilesChange?.(newFiles);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
errors,
|
||||||
|
files: newFiles,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (errors.length > 0) {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
errors,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset input value after handling files
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
state.files,
|
||||||
|
maxFiles,
|
||||||
|
multiple,
|
||||||
|
maxSize,
|
||||||
|
validateFile,
|
||||||
|
createPreview,
|
||||||
|
generateUniqueId,
|
||||||
|
clearFiles,
|
||||||
|
onFilesChange,
|
||||||
|
onFilesAdded,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeFile = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
setState((prev) => {
|
||||||
|
const fileToRemove = prev.files.find((file) => file.id === id);
|
||||||
|
if (
|
||||||
|
fileToRemove?.preview &&
|
||||||
|
fileToRemove.file instanceof File &&
|
||||||
|
fileToRemove.file.type.startsWith("image/")
|
||||||
|
) {
|
||||||
|
URL.revokeObjectURL(fileToRemove.preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFiles = prev.files.filter((file) => file.id !== id);
|
||||||
|
onFilesChange?.(newFiles);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
errors: [],
|
||||||
|
files: newFiles,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onFilesChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearErrors = useCallback(() => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
errors: [],
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setState((prev) => ({ ...prev, isDragging: true }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prev) => ({ ...prev, isDragging: false }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDrop = useCallback(
|
||||||
|
(e: DragEvent<HTMLElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setState((prev) => ({ ...prev, isDragging: false }));
|
||||||
|
|
||||||
|
// Don't process files if the input is disabled
|
||||||
|
if (inputRef.current?.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||||
|
// In single file mode, only use the first file
|
||||||
|
if (!multiple) {
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
addFiles([file]);
|
||||||
|
} else {
|
||||||
|
addFiles(e.dataTransfer.files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addFiles, multiple],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFileChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
|
addFiles(e.target.files);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addFiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openFileDialog = useCallback(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.click();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getInputProps = useCallback(
|
||||||
|
(props: InputHTMLAttributes<HTMLInputElement> = {}) => {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
accept: props.accept || accept,
|
||||||
|
multiple: props.multiple !== undefined ? props.multiple : multiple,
|
||||||
|
onChange: handleFileChange,
|
||||||
|
// Cast to `any` to prevent mismatched React ref type errors across workspaces
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Intentional
|
||||||
|
ref: inputRef as any,
|
||||||
|
type: "file" as const,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[accept, multiple, handleFileChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
addFiles,
|
||||||
|
clearErrors,
|
||||||
|
clearFiles,
|
||||||
|
getInputProps,
|
||||||
|
handleDragEnter,
|
||||||
|
handleDragLeave,
|
||||||
|
handleDragOver,
|
||||||
|
handleDrop,
|
||||||
|
handleFileChange,
|
||||||
|
openFileDialog,
|
||||||
|
removeFile,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to format bytes to human-readable format
|
||||||
|
export const formatBytes = (bytes: number, decimals = 2): string => {
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return Number.parseFloat((bytes / k ** i).toFixed(dm)) + sizes[i];
|
||||||
|
};
|
||||||
@@ -342,6 +342,19 @@ export const guides = pgTable("guides", {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region Donate
|
||||||
|
export const schDonate = pgSchema("donate");
|
||||||
|
|
||||||
|
export const donations = schDonate.table("donations", {
|
||||||
|
id: text().primaryKey().$defaultFn(uuidv7),
|
||||||
|
name: text().notNull(),
|
||||||
|
amount: numeric().notNull(),
|
||||||
|
image: bytea(),
|
||||||
|
message: text(),
|
||||||
|
});
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region Better Auth
|
//#region Better Auth
|
||||||
export const user = pgTable("user", {
|
export const user = pgTable("user", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ export const sse = sseEndpointMap({
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
donate: {
|
||||||
|
ping: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
amount: z.number(),
|
||||||
|
image: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
// roundtrip SSE system check
|
||||||
|
heartbeat: z.number(),
|
||||||
|
update: z.null(),
|
||||||
|
},
|
||||||
// Passive Update Checker
|
// Passive Update Checker
|
||||||
active: {
|
active: {
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
|
|||||||
@@ -22,10 +22,12 @@
|
|||||||
"@bprogress/next": "^3.2.12",
|
"@bprogress/next": "^3.2.12",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@google/genai": "^2.8.0",
|
||||||
"@headless-tree/core": "^1.6.0",
|
"@headless-tree/core": "^1.6.0",
|
||||||
"@headless-tree/react": "^1.6.0",
|
"@headless-tree/react": "^1.6.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@next/bundle-analyzer": "16.2.6",
|
"@next/bundle-analyzer": "16.2.6",
|
||||||
|
"@origin-space/image-cropper": "^0.1.9",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||||
@@ -67,6 +69,7 @@
|
|||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
"motion": "^12.40.0",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
@@ -86,6 +89,7 @@
|
|||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"uuidv7": "^1.1.0",
|
"uuidv7": "^1.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
|
"wav": "^1.0.2",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.8.2",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
@@ -97,6 +101,7 @@
|
|||||||
"@types/node": "^20.19.26",
|
"@types/node": "^20.19.26",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@types/wav": "^1.0.4",
|
||||||
"drizzle-kit": "^0.31.8",
|
"drizzle-kit": "^0.31.8",
|
||||||
"oxfmt": "^0.52.0",
|
"oxfmt": "^0.52.0",
|
||||||
"oxlint": "^1.67.0",
|
"oxlint": "^1.67.0",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Reference in New Issue
Block a user