feat: very barebones search

This commit is contained in:
2025-01-08 19:43:40 +01:00
parent 04b8a91e80
commit ef58db01de
10 changed files with 429 additions and 51 deletions

View File

@@ -8,4 +8,14 @@ services:
volumes:
- ./psql:/var/lib/postgresql/data
ports:
- 5555:5432
- 5555:5432
dragonfly:
image: 'docker.dragonflydb.io/dragonflydb/dragonfly'
ulimits:
memlock: -1
ports:
- "6379:6379"
environment:
DRAGONFLY_PASSWORD: dfsjhkdswkjntelsmldbfvsgknl5t
volumes:
- ./draognfly:/data

View File

@@ -25,6 +25,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.0",
"cron": "^3.3.2",
"ioredis": "^5.4.2",
"lucia": "^3.1.1",
"lucide-react": "^0.368.0",
"minio": "^8.0.3",
@@ -37,6 +38,7 @@
"sonner": "^1.4.41",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"unstorage": "^1.14.4",
"zod": "^3.24.1"
},
"devDependencies": {

View File

@@ -0,0 +1,29 @@
import ephemeralStorage from '@/lib/services/ephemeralStorage';
import type { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q')!.toLowerCase().split(' ').at(-1);
if (query !== '' && !query) {
return new Response('Invalid query', { status: 400 });
}
const tags = await ephemeralStorage.keys('tag');
const filteredTags = tags.filter((tag) => tag.replace('tag:', '').includes(query));
const mappedTags = await Promise.all(
filteredTags.map(async (t) => {
const getTag = parseInt((await ephemeralStorage.get(t)) as string);
return { tag: t.replace('tag:', ''), count: getTag };
})
);
mappedTags.sort((a, b) => {
return b.count - a.count;
});
return new Response(JSON.stringify(mappedTags), {
headers: {
'Content-Type': 'application/json',
},
});
}

View File

@@ -4,14 +4,24 @@ import { type NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = parseInt(searchParams.get('page')!);
const query = searchParams.get('q')?.trim().split(' ')!;
if (page && isNaN(page)) {
return new Response('Invalid page number', { status: 400 });
}
if (!query.length) {
return new Response('Invalid query', { status: 400 });
}
// TODO: negative tags
const queryPosts = await prisma.post.findMany({
take: 30,
skip: page * 30,
orderBy: { createdAt: 'desc' },
where: {
tags: {
hasEvery: query[0] !== '' ? query : [],
},
},
});
return new Response(JSON.stringify(queryPosts), {

View File

@@ -1,10 +1,6 @@
import PostBrowser from '@/components/app/PostBrowser/PostBrowser';
import prisma from '@/lib/db';
import Image from 'next/image';
import Link from 'next/link';
export default async function Home() {
const posts = await prisma.post.findMany({ take: 30, orderBy: { createdAt: 'desc' } });
return (
<div className="text-center pt-2">
<h1>This is nextbooru</h1>

View File

@@ -6,16 +6,24 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Post } from '@prisma/client';
import Image from 'next/image';
import Link from 'next/link';
import Search from '../Search/Search';
export default function PostBrowser() {
const [page, setPage] = React.useState(0);
const [loading, setLoading] = React.useState(false);
const [hasMore, setHasMore] = React.useState(true);
const [posts, setPosts] = React.useState<Post[]>([]);
const [search, setSearch] = React.useState('');
const next = async () => {
const next = async (reset = false, query = search) => {
setLoading(true);
const res = await fetch(`/api/posts?page=${page}`);
const currentPage = reset ? 0 : page;
if (reset) {
setPosts([]);
setPage(0);
setHasMore(true);
}
const res = await fetch(`/api/posts?page=${currentPage}&q=${query}`);
const newImages = await res.json();
if (!newImages.length) {
setHasMore(false);
@@ -28,37 +36,42 @@ export default function PostBrowser() {
};
return (
<InfiniteScroll isLoading={loading} hasMore={hasMore} next={next} threshold={0}>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 p-4">
{posts.map((post) => (
<div key={post.id} className="relative aspect-square">
<Image
width={176}
height={176}
src={post.imageUrl}
alt=""
className="w-full h-full object-contain border-2 rounded-md border-dashed pointer-events-none"
blurDataURL={`data:image/jpeg;base64,${post.previewHash}`}
placeholder="blur"
/>
<Link
href={`/post/${post.id}`}
className="absolute inset-0 z-10 hover:bg-muted-foreground/5"
aria-label="View post details"
/>
</div>
))}
{loading &&
Array.from({ length: page === 0 ? 12 : 2 }).map((_, i) => (
<div key={i} className="relative aspect-square">
<Skeleton className="w-full h-full rounded-md border-2 border-dashed" />
<>
<Search onSearch={q => {
setSearch(q);
next(true, q);
}} />
<InfiniteScroll isLoading={loading} hasMore={hasMore} next={next} threshold={0}>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 p-4">
{posts.map((post) => (
<div key={post.id} className="relative aspect-square">
<Image
width={176}
height={176}
src={post.imageUrl}
alt=""
className="w-full h-full object-contain border-2 rounded-md border-dashed pointer-events-none"
blurDataURL={`data:image/jpeg;base64,${post.previewHash}`}
placeholder="blur"
/>
<Link
href={`/post/${post.id}`}
className="absolute inset-0 z-10 hover:bg-muted-foreground/5"
aria-label="View post details"
/>
</div>
))}
</div>
{loading &&
Array.from({ length: page === 0 ? 12 : 2 }).map((_, i) => (
<div key={i} className="relative aspect-square">
<Skeleton className="w-full h-full rounded-md border-2 border-dashed" />
</div>
))}
</div>
{/* interactionobserver sentinel */}
<div style={{ height: 1 }} />
</InfiniteScroll>
{/* interactionobserver sentinel */}
<div style={{ height: 1 }} />
</InfiniteScroll>
</>
);
}
}

View File

@@ -0,0 +1,108 @@
'use client'
import React, { useState, useEffect, useRef } from 'react'
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Search as SearchIcon } from 'lucide-react'
export default function Search(props: Props) {
const [input, setInput] = useState('')
const [suggestions, setSuggestions] = useState<{ tag: string, count: string }[]>([])
const [selectedIndex, setSelectedIndex] = useState(-1)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
const fetchSuggestions = async () => {
const lastTag = input.split(' ').pop() || ''
if (lastTag.length > 0) {
try {
const response = await fetch(`/api/autocomplete?q=${encodeURIComponent(lastTag)}`)
if (response.ok) {
const data = await response.json()
setSuggestions(data.slice(0, 5))
} else {
setSuggestions([])
}
} catch (error) {
setSuggestions([])
}
} else {
setSuggestions([])
}
setSelectedIndex(-1)
}
fetchSuggestions()
}, [input])
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'ArrowDown') {
setSelectedIndex(prev => (prev < suggestions.length - 1 ? prev + 1 : prev))
e.preventDefault()
} else if (e.key === 'ArrowUp') {
setSelectedIndex(prev => (prev > 0 ? prev - 1 : -1))
e.preventDefault()
} else if (e.key === 'Enter' && selectedIndex >= 0) {
const tags = input.split(' ').slice(0, -1)
tags.push(suggestions[selectedIndex].tag)
setInput(tags.join(' ') + ' ')
setSuggestions([])
e.preventDefault()
} else if (e.key === 'Escape') {
setSuggestions([])
} else if (e.key === 'Enter') {
props.onSearch(input)
}
}
const handleSuggestionClick = (suggestion: string) => {
const tags = input.split(' ').slice(0, -1)
tags.push(suggestion)
setInput(tags.join(' ') + ' ')
setSuggestions([])
inputRef.current?.focus()
}
return (
<div className="w-full max-w-md mx-auto mt-10">
<div className="relative">
<Input
ref={inputRef}
type="text"
placeholder="Enter tags..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
className="pr-10 font-mono"
/>
<Button
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => props.onSearch(input)}
>
<SearchIcon className="h-4 w-4" />
<span className="sr-only">Search</span>
</Button>
</div>
{suggestions.length > 0 && (
<ul className="mt-1 bg-background border rounded-md shadow-lg max-h-60 overflow-auto">
{suggestions.map((suggestion, index) => (
<li
key={suggestion.tag}
className={`px-4 py-2 cursor-pointer hover:bg-muted ${
index === selectedIndex ? 'bg-muted' : ''
}`}
onClick={() => handleSuggestionClick(suggestion.tag)}
>
{suggestion.tag} ({suggestion.count})
</li>
))}
</ul>
)}
</div>
)
}
interface Props {
onSearch: (q: string) => void
}

View File

@@ -1,16 +1,16 @@
import prisma from './lib/db';
import ephemeralStorage from './lib/services/ephemeralStorage';
export async function register() {
if (process.env.SAFEBOORU_PULL !== 'true') return;
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { CronJob } = await import('cron');
const crypto = await import('crypto');
const { generateId } = await import('lucia');
const fs = await import('fs/promises');
const { default: hashImage } = await import('@/lib/hashImage');
const minio = (await import('@/lib/services/minio')).default;
const job = async () => {
const { performance } = await import('perf_hooks');
const safebooruJob = async () => {
if (process.env.SAFEBOORU_PULL !== 'true') return;
console.log('Deleting prior safebooru posts and accounts...');
await prisma.post.deleteMany({
where: { author: { username: { startsWith: 'safebooru-' } } },
@@ -28,14 +28,13 @@ export async function register() {
console.log('Pulling safebooru images...');
console.log('Fetching...');
const res = await fetch(
'https://safebooru.org/index.php?page=dapi&s=post&q=index&json=1&limit=40',
'https://safebooru.org/index.php?page=dapi&s=post&q=index&json=1&limit=300',
{ headers: { 'User-Agent': 'nextbooru' } }
);
const posts = await res.json();
const genId = generateId(6);
console.log('Creating account...');
const genId = generateId(6);
const account = await prisma.user.create({
data: {
username: `safebooru-${genId}`,
@@ -49,7 +48,6 @@ export async function register() {
const savedFilename = `http${minioIsSSL ? 's' : ''}://${process.env.MINIO_ENDPOINT}/${
process.env.MINIO_BUCKET
}/safebooru-${genId}-${post.id}.jpg`;
//await fs.writeFile(savedFilename, new Uint8Array(imageUrl));
await minio.putObject(
process.env.MINIO_BUCKET!,
`safebooru-${genId}-${post.id}.jpg`,
@@ -68,9 +66,30 @@ export async function register() {
console.log(`Downloaded id ${post.id}`);
}
};
// await safebooruJob();
await job();
const writeTagsToEphemeral = async () => {
// TODO: move tags to another table. this is a temporary and inefficient solution.
const perfStart = performance.now();
const posts = await prisma.post.findMany({ select: { tags: true } });
const tags = posts.flatMap((post) => post.tags);
// new CronJob('0 */2 * * *', async () => await job(), null, true);
const occurrences: Record<string, number> = {};
for (const tag of tags) {
if (occurrences[tag]) {
occurrences[tag]++;
} else {
occurrences[tag] = 1;
}
}
for (const [tag, count] of Object.entries(occurrences)) {
await ephemeralStorage.set(`tag:${tag}`, count);
}
const perfEnd = performance.now();
console.log(`Writing tags to ephemeral took ${Math.round(perfEnd - perfStart)}ms`);
}
await writeTagsToEphemeral();
}
}

View File

@@ -0,0 +1,12 @@
import { createStorage } from "unstorage";
import redisDriver from 'unstorage/drivers/redis';
const ephemeralStorage = createStorage({
driver: redisDriver({
host: process.env.REDIS_HOST,
port: Number(process.env.REDIS_PORT),
password: process.env.REDIS_PASSWORD,
}),
});
export default ephemeralStorage;

183
yarn.lock
View File

@@ -473,6 +473,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342"
integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==
"@ioredis/commands@^1.1.1":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -1430,7 +1435,7 @@ any-promise@^1.0.0:
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
anymatch@~3.1.2:
anymatch@^3.1.3, anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
@@ -1760,7 +1765,7 @@ chalk@^5.0.0:
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8"
integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==
chokidar@^3.5.3:
chokidar@^3.5.3, chokidar@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
@@ -1809,6 +1814,11 @@ clsx@^2.1.0, clsx@^2.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
code-block-writer@^12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770"
@@ -1857,11 +1867,21 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
consola@^3.2.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/consola/-/consola-3.3.3.tgz#0dd8a2314b0f7bf18a49064138ad685f3346543d"
integrity sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg==
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
cookie-es@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.2.2.tgz#18ceef9eb513cac1cb6c14bcbf8bdb2679b34821"
integrity sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==
cosmiconfig@^8.1.3:
version "8.3.6"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3"
@@ -1898,6 +1918,13 @@ cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
"crossws@>=0.2.0 <0.4.0":
version "0.3.1"
resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.3.1.tgz#7980e0b6688fe23286661c3ab8deeccbaa05ca86"
integrity sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==
dependencies:
uncrypto "^0.1.3"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@@ -2006,11 +2033,26 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
defu@^6.1.4:
version "6.1.4"
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479"
integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==
denque@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
dequal@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
destr@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449"
integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==
detect-libc@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
@@ -2792,6 +2834,22 @@ graphemer@^1.4.0:
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
h3@^1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/h3/-/h3-1.13.0.tgz#b5347a8936529794b6754b440e26c0ab8a60dceb"
integrity sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==
dependencies:
cookie-es "^1.2.2"
crossws ">=0.2.0 <0.4.0"
defu "^6.1.4"
destr "^2.0.3"
iron-webcrypto "^1.2.1"
ohash "^1.1.4"
radix3 "^1.1.2"
ufo "^1.5.4"
uncrypto "^0.1.3"
unenv "^1.10.0"
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@@ -2903,11 +2961,31 @@ invariant@^2.2.4:
dependencies:
loose-envify "^1.0.0"
ioredis@^5.4.2:
version "5.4.2"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.2.tgz#ebb6f1a10b825b2c0fb114763d7e82114a0bee6c"
integrity sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==
dependencies:
"@ioredis/commands" "^1.1.1"
cluster-key-slot "^1.1.0"
debug "^4.3.4"
denque "^2.1.0"
lodash.defaults "^4.2.0"
lodash.isarguments "^3.1.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
standard-as-callback "^2.1.0"
ipaddr.js@^2.0.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8"
integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==
iron-webcrypto@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz#aa60ff2aa10550630f4c0b11fd2442becdb35a6f"
integrity sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==
is-arguments@^1.0.4:
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b"
@@ -3327,6 +3405,16 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==
lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==
lodash.isarguments@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -3372,6 +3460,11 @@ lru-cache@^10.2.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.1.tgz#e8d901141f22937968e45a6533d52824070151e4"
integrity sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA==
lru-cache@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -3452,6 +3545,11 @@ mime-types@^2.1.35:
dependencies:
mime-db "1.52.0"
mime@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@@ -3587,6 +3685,11 @@ node-domexception@^1.0.0:
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch-native@^1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e"
integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==
node-fetch@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b"
@@ -3689,6 +3792,20 @@ object.values@^1.1.6, object.values@^1.1.7:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
ofetch@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.4.1.tgz#b6bf6b0d75ba616cef6519dd8b6385a8bae480ec"
integrity sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==
dependencies:
destr "^2.0.3"
node-fetch-native "^1.6.4"
ufo "^1.5.4"
ohash@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72"
integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -3819,6 +3936,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pathe@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec"
integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==
picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
@@ -3957,6 +4079,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
radix3@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.1.2.tgz#fd27d2af3896c6bf4bcdfab6427c69c2afc69ec0"
integrity sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==
react-dom@19:
version "19.0.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57"
@@ -4041,6 +4168,18 @@ recast@^0.23.2:
tiny-invariant "^1.3.3"
tslib "^2.0.1"
redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
redis-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
dependencies:
redis-errors "^1.0.0"
reflect.getprototypeof@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859"
@@ -4333,6 +4472,11 @@ split-on-first@^1.0.0:
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
standard-as-callback@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
stdin-discarder@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz#22b3e400393a8e28ebf53f9958f3880622efde21"
@@ -4715,6 +4859,11 @@ typescript@^5:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
ufo@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"
integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
@@ -4725,16 +4874,46 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
uncrypto@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b"
integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
unenv@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.10.0.tgz#c3394a6c6e4cfe68d699f87af456fe3f0db39571"
integrity sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==
dependencies:
consola "^3.2.3"
defu "^6.1.4"
mime "^3.0.0"
node-fetch-native "^1.6.4"
pathe "^1.1.2"
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unstorage@^1.14.4:
version "1.14.4"
resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.14.4.tgz#620dd68997a3245fca1e04c0171335817525bc3d"
integrity sha512-1SYeamwuYeQJtJ/USE1x4l17LkmQBzg7deBJ+U9qOBoHo15d1cDxG4jM31zKRgF7pG0kirZy4wVMX6WL6Zoscg==
dependencies:
anymatch "^3.1.3"
chokidar "^3.6.0"
destr "^2.0.3"
h3 "^1.13.0"
lru-cache "^10.4.3"
node-fetch-native "^1.6.4"
ofetch "^1.4.1"
ufo "^1.5.4"
update-browserslist-db@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"