Spaces:
Running
Running
Deploy Next.js Docker Space from 5bdfbf8
Browse files- .env.agent.example +30 -0
- .env.example +69 -0
- .prettierrc.json +21 -21
- LICENSE +21 -21
- components.json +20 -20
- postcss.config.mjs +5 -5
- public/favicon.svg +18 -18
- src/app/api/auth-status/route.ts +6 -6
- src/components/theme-provider.tsx +8 -8
- src/components/ui/alert.tsx +66 -66
- src/components/ui/card.tsx +92 -92
- src/components/ui/label.tsx +24 -24
- src/components/ui/textarea.tsx +18 -18
- src/components/ui/toggle-group.tsx +73 -73
- src/lib/db.ts +22 -22
- src/lib/utils.ts +6 -6
- tsconfig.json +37 -37
.env.agent.example
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent API 访问令牌。正式部署请使用足够长的随机值。
|
| 2 |
+
AGENT_API_TOKEN=
|
| 3 |
+
|
| 4 |
+
# 状态后端:HF Space 免费层或临时演示用 memory,本地单实例用 sqlite,高并发状态用 postgres。多实例还需共享 generated-images。
|
| 5 |
+
AGENT_STATE_BACKEND=sqlite
|
| 6 |
+
|
| 7 |
+
# SQLite 默认状态文件。保留在 generated-images 下,方便 Docker volume 备份同时包含元数据。
|
| 8 |
+
AGENT_SQLITE_PATH=generated-images/.agent-state/agent.sqlite
|
| 9 |
+
|
| 10 |
+
# PostgreSQL 配置。Docker Compose 模式优先使用 secret file;手动部署也可以直接提供 AGENT_DATABASE_URL。
|
| 11 |
+
# GPT_IMAGE_POSTGRES_PASSWORD=<database-password>
|
| 12 |
+
# AGENT_DB_HOST=postgres
|
| 13 |
+
# AGENT_DB_PORT=5432
|
| 14 |
+
# AGENT_DB_NAME=gpt_image_playground
|
| 15 |
+
# AGENT_DB_USER=gpt_image
|
| 16 |
+
# AGENT_DB_PASSWORD=<database-password>
|
| 17 |
+
# AGENT_DATABASE_URL=postgres://gpt_image:<database-password>@postgres:5432/gpt_image_playground
|
| 18 |
+
|
| 19 |
+
# 请求租约和幂等记录保留时间。
|
| 20 |
+
AGENT_REQUEST_LEASE_MS=600000
|
| 21 |
+
AGENT_REQUEST_TTL_SECONDS=86400
|
| 22 |
+
AGENT_RECOVERY_INTERVAL_MS=30000
|
| 23 |
+
|
| 24 |
+
# 可选:供 OpenAPI 客户端使用的公开基础地址。
|
| 25 |
+
# AGENT_PUBLIC_BASE_URL=http://localhost:4783
|
| 26 |
+
|
| 27 |
+
# 现有应用配置仍然生效。
|
| 28 |
+
OPENAI_API_KEY=
|
| 29 |
+
OPENAI_API_BASE_URL=https://api.openai.com/v1
|
| 30 |
+
APP_PASSWORD=
|
.env.example
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 可选:如果不想在网页右上角“API 设置”里填写,可以在这里配置默认值。
|
| 2 |
+
# 使用时复制本文件为 .env.local,再填写真实内容。
|
| 3 |
+
|
| 4 |
+
# OpenAI 或 OpenAI 兼容接口 Key
|
| 5 |
+
OPENAI_API_KEY=
|
| 6 |
+
|
| 7 |
+
# 可选:OpenAI 兼容接口根地址,通常以 /v1 结尾。
|
| 8 |
+
# 示例:https://api.openai.com/v1
|
| 9 |
+
OPENAI_API_BASE_URL=
|
| 10 |
+
|
| 11 |
+
# 可选:服务端多渠道多 key 配置。配置任意 OPENAI_CHANNEL_N_* 后,会优先于 OPENAI_API_KEY。
|
| 12 |
+
# 页面右上角“API 设置”里手动填写的 API Key/API URL 仍然拥有最高优先级。
|
| 13 |
+
#
|
| 14 |
+
# 路由策略:
|
| 15 |
+
# - sticky:默认值。按请求来源稳定映射到同一个渠道 key,适合减少同一用户跳渠道。
|
| 16 |
+
# - round_robin:按请求顺序轮询所有渠道 key,适合简单均摊流量。
|
| 17 |
+
# - random:每次随机选择一个渠道 key,适合轻量分散请求。
|
| 18 |
+
# OPENAI_ROUTING_STRATEGY=sticky
|
| 19 |
+
#
|
| 20 |
+
# 渠道配置规则:
|
| 21 |
+
# - N 从 1 开始递增,例如 OPENAI_CHANNEL_1_*、OPENAI_CHANNEL_2_*。
|
| 22 |
+
# - ID 只用于日志排查,不会暴露 API Key。
|
| 23 |
+
# - BASE_URL 是 OpenAI 兼容接口根地址,通常以 /v1 结尾。
|
| 24 |
+
# - API_KEYS 支持一个或多个 key,多个 key 用英文逗号分隔。
|
| 25 |
+
# - FAILURE_COOLDOWN_MS 可选,覆盖该渠道失败后的冷却时间。
|
| 26 |
+
# - API Key 本身不要包含逗号。
|
| 27 |
+
#
|
| 28 |
+
# 示例:
|
| 29 |
+
# OPENAI_CHANNEL_1_ID=official
|
| 30 |
+
# OPENAI_CHANNEL_1_BASE_URL=https://api.openai.com/v1
|
| 31 |
+
# OPENAI_CHANNEL_1_API_KEYS=sk-key-1,sk-key-2
|
| 32 |
+
# OPENAI_CHANNEL_1_FAILURE_COOLDOWN_MS=60000
|
| 33 |
+
#
|
| 34 |
+
# OPENAI_CHANNEL_2_ID=backup
|
| 35 |
+
# OPENAI_CHANNEL_2_BASE_URL=https://your-compatible-api.example.com/v1
|
| 36 |
+
# OPENAI_CHANNEL_2_API_KEYS=sk-backup-1
|
| 37 |
+
|
| 38 |
+
# 可选:并发流式批处理。默认关闭。
|
| 39 |
+
# 开启后,流式模式下 n>1 会拆成多个 n=1 的独立流式任务,并按服务端 key 容量并发执行。
|
| 40 |
+
# 默认 sticky 路由按单个 credential 容量推荐并发;round_robin/random 才会使用完整 credential 池。
|
| 41 |
+
# key 出现鉴权、额度或限流类错误后会短暂冷却;渠道出现 5xx、CDN 超时或连接错误后会冷却整个渠道。
|
| 42 |
+
# ENABLE_STREAMING_BATCH=true
|
| 43 |
+
# OPENAI_MAX_STREAMS_PER_CREDENTIAL=1
|
| 44 |
+
# OPENAI_CHANNEL_FAILURE_COOLDOWN_MS=60000
|
| 45 |
+
|
| 46 |
+
# 可选:实验 Responses API 图片后端。默认关闭。
|
| 47 |
+
# 开启后仍需请求显式传入 imageBackend=responses,并单独配置 /responses 顶层模型。
|
| 48 |
+
# 表单 model 只作为 image_generation 工具模型,不会复用为 /responses 顶层模型。
|
| 49 |
+
# ENABLE_RESPONSES_IMAGE_BACKEND=true
|
| 50 |
+
# OPENAI_RESPONSES_API_MODEL=
|
| 51 |
+
|
| 52 |
+
# 可选:给网页加一个访问密码。公网部署时建议一定要设置。
|
| 53 |
+
APP_PASSWORD=
|
| 54 |
+
|
| 55 |
+
# 可选:给 /api/agent/* 使用的 Bearer token。公网或内网共享部署时建议设置。
|
| 56 |
+
# AGENT_API_TOKEN=
|
| 57 |
+
|
| 58 |
+
# 可选:Agent 状态后端。memory 适合临时演示,sqlite 适合单实例本地部署,postgres 适合状态并发;多实例还需共享 generated-images。
|
| 59 |
+
# AGENT_STATE_BACKEND=sqlite
|
| 60 |
+
# AGENT_SQLITE_PATH=generated-images/.agent-state/agent.sqlite
|
| 61 |
+
# AGENT_DATABASE_URL=postgres://gpt_image:<database-password>@localhost:5432/gpt_image_playground
|
| 62 |
+
# AGENT_REQUEST_LEASE_MS=600000
|
| 63 |
+
# AGENT_REQUEST_TTL_SECONDS=86400
|
| 64 |
+
|
| 65 |
+
# 可选:服务端日志等级。可选 debug、info、warn、error。生产环境默认 warn,其他环境默认 info。
|
| 66 |
+
# APP_LOG_LEVEL=warn
|
| 67 |
+
|
| 68 |
+
# 本地运行一般不用设置;部署到 Vercel 时建议使用 indexeddb。
|
| 69 |
+
# NEXT_PUBLIC_IMAGE_STORAGE_MODE=indexeddb
|
.prettierrc.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
| 1 |
-
{
|
| 2 |
-
"arrowParens": "always",
|
| 3 |
-
"bracketSpacing": true,
|
| 4 |
-
"htmlWhitespaceSensitivity": "css",
|
| 5 |
-
"insertPragma": false,
|
| 6 |
-
"bracketSameLine": true,
|
| 7 |
-
"jsxSingleQuote": true,
|
| 8 |
-
"printWidth": 120,
|
| 9 |
-
"proseWrap": "preserve",
|
| 10 |
-
"quoteProps": "as-needed",
|
| 11 |
-
"requirePragma": false,
|
| 12 |
-
"semi": true,
|
| 13 |
-
"singleQuote": true,
|
| 14 |
-
"tabWidth": 4,
|
| 15 |
-
"trailingComma": "none",
|
| 16 |
-
"useTabs": false,
|
| 17 |
-
"endOfLine": "auto",
|
| 18 |
-
"plugins": [
|
| 19 |
-
"@trivago/prettier-plugin-sort-imports",
|
| 20 |
-
"prettier-plugin-tailwindcss"
|
| 21 |
-
]
|
| 22 |
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"arrowParens": "always",
|
| 3 |
+
"bracketSpacing": true,
|
| 4 |
+
"htmlWhitespaceSensitivity": "css",
|
| 5 |
+
"insertPragma": false,
|
| 6 |
+
"bracketSameLine": true,
|
| 7 |
+
"jsxSingleQuote": true,
|
| 8 |
+
"printWidth": 120,
|
| 9 |
+
"proseWrap": "preserve",
|
| 10 |
+
"quoteProps": "as-needed",
|
| 11 |
+
"requirePragma": false,
|
| 12 |
+
"semi": true,
|
| 13 |
+
"singleQuote": true,
|
| 14 |
+
"tabWidth": 4,
|
| 15 |
+
"trailingComma": "none",
|
| 16 |
+
"useTabs": false,
|
| 17 |
+
"endOfLine": "auto",
|
| 18 |
+
"plugins": [
|
| 19 |
+
"@trivago/prettier-plugin-sort-imports",
|
| 20 |
+
"prettier-plugin-tailwindcss"
|
| 21 |
+
]
|
| 22 |
}
|
LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
| 1 |
-
MIT License
|
| 2 |
-
|
| 3 |
-
Copyright (c) 2025 Aljosa Asanovic
|
| 4 |
-
|
| 5 |
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
-
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
-
in the Software without restriction, including without limitation the rights
|
| 8 |
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
-
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
-
furnished to do so, subject to the following conditions:
|
| 11 |
-
|
| 12 |
-
The above copyright notice and this permission notice shall be included in all
|
| 13 |
-
copies or substantial portions of the Software.
|
| 14 |
-
|
| 15 |
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
-
SOFTWARE.
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Aljosa Asanovic
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
components.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
| 1 |
-
{
|
| 2 |
-
"$schema": "https://ui.shadcn.com/schema.json",
|
| 3 |
-
"style": "new-york",
|
| 4 |
-
"rsc": true,
|
| 5 |
-
"tsx": true,
|
| 6 |
-
"tailwind": {
|
| 7 |
-
"config": "",
|
| 8 |
-
"css": "src/app/globals.css",
|
| 9 |
-
"baseColor": "neutral",
|
| 10 |
-
"cssVariables": true,
|
| 11 |
-
"prefix": ""
|
| 12 |
-
},
|
| 13 |
-
"aliases": {
|
| 14 |
-
"components": "@/components",
|
| 15 |
-
"utils": "@/lib/utils",
|
| 16 |
-
"ui": "@/components/ui",
|
| 17 |
-
"lib": "@/lib",
|
| 18 |
-
"hooks": "@/hooks"
|
| 19 |
-
},
|
| 20 |
-
"iconLibrary": "lucide"
|
| 21 |
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
| 3 |
+
"style": "new-york",
|
| 4 |
+
"rsc": true,
|
| 5 |
+
"tsx": true,
|
| 6 |
+
"tailwind": {
|
| 7 |
+
"config": "",
|
| 8 |
+
"css": "src/app/globals.css",
|
| 9 |
+
"baseColor": "neutral",
|
| 10 |
+
"cssVariables": true,
|
| 11 |
+
"prefix": ""
|
| 12 |
+
},
|
| 13 |
+
"aliases": {
|
| 14 |
+
"components": "@/components",
|
| 15 |
+
"utils": "@/lib/utils",
|
| 16 |
+
"ui": "@/components/ui",
|
| 17 |
+
"lib": "@/lib",
|
| 18 |
+
"hooks": "@/hooks"
|
| 19 |
+
},
|
| 20 |
+
"iconLibrary": "lucide"
|
| 21 |
}
|
postcss.config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
const config = {
|
| 2 |
-
plugins: ["@tailwindcss/postcss"],
|
| 3 |
-
};
|
| 4 |
-
|
| 5 |
-
export default config;
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: ["@tailwindcss/postcss"],
|
| 3 |
+
};
|
| 4 |
+
|
| 5 |
+
export default config;
|
public/favicon.svg
CHANGED
|
|
|
|
src/app/api/auth-status/route.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
import { NextResponse } from 'next/server';
|
| 2 |
-
|
| 3 |
-
export async function GET() {
|
| 4 |
-
const appPasswordSet = !!process.env.APP_PASSWORD;
|
| 5 |
-
return NextResponse.json({ passwordRequired: appPasswordSet });
|
| 6 |
-
}
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server';
|
| 2 |
+
|
| 3 |
+
export async function GET() {
|
| 4 |
+
const appPasswordSet = !!process.env.APP_PASSWORD;
|
| 5 |
+
return NextResponse.json({ passwordRequired: appPasswordSet });
|
| 6 |
+
}
|
src/components/theme-provider.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from 'next-themes';
|
| 4 |
-
import * as React from 'react';
|
| 5 |
-
|
| 6 |
-
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
| 7 |
-
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
| 8 |
-
}
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from 'next-themes';
|
| 4 |
+
import * as React from 'react';
|
| 5 |
+
|
| 6 |
+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
| 7 |
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
| 8 |
+
}
|
src/components/ui/alert.tsx
CHANGED
|
@@ -1,66 +1,66 @@
|
|
| 1 |
-
import * as React from "react"
|
| 2 |
-
import { cva, type VariantProps } from "class-variance-authority"
|
| 3 |
-
|
| 4 |
-
import { cn } from "@/lib/utils"
|
| 5 |
-
|
| 6 |
-
const alertVariants = cva(
|
| 7 |
-
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
| 8 |
-
{
|
| 9 |
-
variants: {
|
| 10 |
-
variant: {
|
| 11 |
-
default: "bg-card text-card-foreground",
|
| 12 |
-
destructive:
|
| 13 |
-
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
| 14 |
-
},
|
| 15 |
-
},
|
| 16 |
-
defaultVariants: {
|
| 17 |
-
variant: "default",
|
| 18 |
-
},
|
| 19 |
-
}
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
function Alert({
|
| 23 |
-
className,
|
| 24 |
-
variant,
|
| 25 |
-
...props
|
| 26 |
-
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
| 27 |
-
return (
|
| 28 |
-
<div
|
| 29 |
-
data-slot="alert"
|
| 30 |
-
role="alert"
|
| 31 |
-
className={cn(alertVariants({ variant }), className)}
|
| 32 |
-
{...props}
|
| 33 |
-
/>
|
| 34 |
-
)
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
| 38 |
-
return (
|
| 39 |
-
<div
|
| 40 |
-
data-slot="alert-title"
|
| 41 |
-
className={cn(
|
| 42 |
-
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
| 43 |
-
className
|
| 44 |
-
)}
|
| 45 |
-
{...props}
|
| 46 |
-
/>
|
| 47 |
-
)
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
function AlertDescription({
|
| 51 |
-
className,
|
| 52 |
-
...props
|
| 53 |
-
}: React.ComponentProps<"div">) {
|
| 54 |
-
return (
|
| 55 |
-
<div
|
| 56 |
-
data-slot="alert-description"
|
| 57 |
-
className={cn(
|
| 58 |
-
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
| 59 |
-
className
|
| 60 |
-
)}
|
| 61 |
-
{...props}
|
| 62 |
-
/>
|
| 63 |
-
)
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
export { Alert, AlertTitle, AlertDescription }
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const alertVariants = cva(
|
| 7 |
+
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
| 8 |
+
{
|
| 9 |
+
variants: {
|
| 10 |
+
variant: {
|
| 11 |
+
default: "bg-card text-card-foreground",
|
| 12 |
+
destructive:
|
| 13 |
+
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
| 14 |
+
},
|
| 15 |
+
},
|
| 16 |
+
defaultVariants: {
|
| 17 |
+
variant: "default",
|
| 18 |
+
},
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
function Alert({
|
| 23 |
+
className,
|
| 24 |
+
variant,
|
| 25 |
+
...props
|
| 26 |
+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
| 27 |
+
return (
|
| 28 |
+
<div
|
| 29 |
+
data-slot="alert"
|
| 30 |
+
role="alert"
|
| 31 |
+
className={cn(alertVariants({ variant }), className)}
|
| 32 |
+
{...props}
|
| 33 |
+
/>
|
| 34 |
+
)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
| 38 |
+
return (
|
| 39 |
+
<div
|
| 40 |
+
data-slot="alert-title"
|
| 41 |
+
className={cn(
|
| 42 |
+
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
| 43 |
+
className
|
| 44 |
+
)}
|
| 45 |
+
{...props}
|
| 46 |
+
/>
|
| 47 |
+
)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function AlertDescription({
|
| 51 |
+
className,
|
| 52 |
+
...props
|
| 53 |
+
}: React.ComponentProps<"div">) {
|
| 54 |
+
return (
|
| 55 |
+
<div
|
| 56 |
+
data-slot="alert-description"
|
| 57 |
+
className={cn(
|
| 58 |
+
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
| 59 |
+
className
|
| 60 |
+
)}
|
| 61 |
+
{...props}
|
| 62 |
+
/>
|
| 63 |
+
)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export { Alert, AlertTitle, AlertDescription }
|
src/components/ui/card.tsx
CHANGED
|
@@ -1,92 +1,92 @@
|
|
| 1 |
-
import * as React from "react"
|
| 2 |
-
|
| 3 |
-
import { cn } from "@/lib/utils"
|
| 4 |
-
|
| 5 |
-
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
| 6 |
-
return (
|
| 7 |
-
<div
|
| 8 |
-
data-slot="card"
|
| 9 |
-
className={cn(
|
| 10 |
-
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
| 11 |
-
className
|
| 12 |
-
)}
|
| 13 |
-
{...props}
|
| 14 |
-
/>
|
| 15 |
-
)
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
| 19 |
-
return (
|
| 20 |
-
<div
|
| 21 |
-
data-slot="card-header"
|
| 22 |
-
className={cn(
|
| 23 |
-
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
| 24 |
-
className
|
| 25 |
-
)}
|
| 26 |
-
{...props}
|
| 27 |
-
/>
|
| 28 |
-
)
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
| 32 |
-
return (
|
| 33 |
-
<div
|
| 34 |
-
data-slot="card-title"
|
| 35 |
-
className={cn("leading-none font-semibold", className)}
|
| 36 |
-
{...props}
|
| 37 |
-
/>
|
| 38 |
-
)
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
| 42 |
-
return (
|
| 43 |
-
<div
|
| 44 |
-
data-slot="card-description"
|
| 45 |
-
className={cn("text-muted-foreground text-sm", className)}
|
| 46 |
-
{...props}
|
| 47 |
-
/>
|
| 48 |
-
)
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
| 52 |
-
return (
|
| 53 |
-
<div
|
| 54 |
-
data-slot="card-action"
|
| 55 |
-
className={cn(
|
| 56 |
-
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
| 57 |
-
className
|
| 58 |
-
)}
|
| 59 |
-
{...props}
|
| 60 |
-
/>
|
| 61 |
-
)
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
| 65 |
-
return (
|
| 66 |
-
<div
|
| 67 |
-
data-slot="card-content"
|
| 68 |
-
className={cn("px-6", className)}
|
| 69 |
-
{...props}
|
| 70 |
-
/>
|
| 71 |
-
)
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
| 75 |
-
return (
|
| 76 |
-
<div
|
| 77 |
-
data-slot="card-footer"
|
| 78 |
-
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
| 79 |
-
{...props}
|
| 80 |
-
/>
|
| 81 |
-
)
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
export {
|
| 85 |
-
Card,
|
| 86 |
-
CardHeader,
|
| 87 |
-
CardFooter,
|
| 88 |
-
CardTitle,
|
| 89 |
-
CardAction,
|
| 90 |
-
CardDescription,
|
| 91 |
-
CardContent,
|
| 92 |
-
}
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
| 6 |
+
return (
|
| 7 |
+
<div
|
| 8 |
+
data-slot="card"
|
| 9 |
+
className={cn(
|
| 10 |
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
| 11 |
+
className
|
| 12 |
+
)}
|
| 13 |
+
{...props}
|
| 14 |
+
/>
|
| 15 |
+
)
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
| 19 |
+
return (
|
| 20 |
+
<div
|
| 21 |
+
data-slot="card-header"
|
| 22 |
+
className={cn(
|
| 23 |
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
| 24 |
+
className
|
| 25 |
+
)}
|
| 26 |
+
{...props}
|
| 27 |
+
/>
|
| 28 |
+
)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
| 32 |
+
return (
|
| 33 |
+
<div
|
| 34 |
+
data-slot="card-title"
|
| 35 |
+
className={cn("leading-none font-semibold", className)}
|
| 36 |
+
{...props}
|
| 37 |
+
/>
|
| 38 |
+
)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
| 42 |
+
return (
|
| 43 |
+
<div
|
| 44 |
+
data-slot="card-description"
|
| 45 |
+
className={cn("text-muted-foreground text-sm", className)}
|
| 46 |
+
{...props}
|
| 47 |
+
/>
|
| 48 |
+
)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
| 52 |
+
return (
|
| 53 |
+
<div
|
| 54 |
+
data-slot="card-action"
|
| 55 |
+
className={cn(
|
| 56 |
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
| 57 |
+
className
|
| 58 |
+
)}
|
| 59 |
+
{...props}
|
| 60 |
+
/>
|
| 61 |
+
)
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
| 65 |
+
return (
|
| 66 |
+
<div
|
| 67 |
+
data-slot="card-content"
|
| 68 |
+
className={cn("px-6", className)}
|
| 69 |
+
{...props}
|
| 70 |
+
/>
|
| 71 |
+
)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
| 75 |
+
return (
|
| 76 |
+
<div
|
| 77 |
+
data-slot="card-footer"
|
| 78 |
+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
| 79 |
+
{...props}
|
| 80 |
+
/>
|
| 81 |
+
)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
export {
|
| 85 |
+
Card,
|
| 86 |
+
CardHeader,
|
| 87 |
+
CardFooter,
|
| 88 |
+
CardTitle,
|
| 89 |
+
CardAction,
|
| 90 |
+
CardDescription,
|
| 91 |
+
CardContent,
|
| 92 |
+
}
|
src/components/ui/label.tsx
CHANGED
|
@@ -1,24 +1,24 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import * as React from "react"
|
| 4 |
-
import * as LabelPrimitive from "@radix-ui/react-label"
|
| 5 |
-
|
| 6 |
-
import { cn } from "@/lib/utils"
|
| 7 |
-
|
| 8 |
-
function Label({
|
| 9 |
-
className,
|
| 10 |
-
...props
|
| 11 |
-
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
| 12 |
-
return (
|
| 13 |
-
<LabelPrimitive.Root
|
| 14 |
-
data-slot="label"
|
| 15 |
-
className={cn(
|
| 16 |
-
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
| 17 |
-
className
|
| 18 |
-
)}
|
| 19 |
-
{...props}
|
| 20 |
-
/>
|
| 21 |
-
)
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
export { Label }
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Label({
|
| 9 |
+
className,
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
| 12 |
+
return (
|
| 13 |
+
<LabelPrimitive.Root
|
| 14 |
+
data-slot="label"
|
| 15 |
+
className={cn(
|
| 16 |
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
| 17 |
+
className
|
| 18 |
+
)}
|
| 19 |
+
{...props}
|
| 20 |
+
/>
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export { Label }
|
src/components/ui/textarea.tsx
CHANGED
|
@@ -1,18 +1,18 @@
|
|
| 1 |
-
import * as React from "react"
|
| 2 |
-
|
| 3 |
-
import { cn } from "@/lib/utils"
|
| 4 |
-
|
| 5 |
-
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
| 6 |
-
return (
|
| 7 |
-
<textarea
|
| 8 |
-
data-slot="textarea"
|
| 9 |
-
className={cn(
|
| 10 |
-
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
| 11 |
-
className
|
| 12 |
-
)}
|
| 13 |
-
{...props}
|
| 14 |
-
/>
|
| 15 |
-
)
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
export { Textarea }
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
| 6 |
+
return (
|
| 7 |
+
<textarea
|
| 8 |
+
data-slot="textarea"
|
| 9 |
+
className={cn(
|
| 10 |
+
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
| 11 |
+
className
|
| 12 |
+
)}
|
| 13 |
+
{...props}
|
| 14 |
+
/>
|
| 15 |
+
)
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export { Textarea }
|
src/components/ui/toggle-group.tsx
CHANGED
|
@@ -1,73 +1,73 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import * as React from "react"
|
| 4 |
-
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
| 5 |
-
import { type VariantProps } from "class-variance-authority"
|
| 6 |
-
|
| 7 |
-
import { cn } from "@/lib/utils"
|
| 8 |
-
import { toggleVariants } from "@/components/ui/toggle"
|
| 9 |
-
|
| 10 |
-
const ToggleGroupContext = React.createContext<
|
| 11 |
-
VariantProps<typeof toggleVariants>
|
| 12 |
-
>({
|
| 13 |
-
size: "default",
|
| 14 |
-
variant: "default",
|
| 15 |
-
})
|
| 16 |
-
|
| 17 |
-
function ToggleGroup({
|
| 18 |
-
className,
|
| 19 |
-
variant,
|
| 20 |
-
size,
|
| 21 |
-
children,
|
| 22 |
-
...props
|
| 23 |
-
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
| 24 |
-
VariantProps<typeof toggleVariants>) {
|
| 25 |
-
return (
|
| 26 |
-
<ToggleGroupPrimitive.Root
|
| 27 |
-
data-slot="toggle-group"
|
| 28 |
-
data-variant={variant}
|
| 29 |
-
data-size={size}
|
| 30 |
-
className={cn(
|
| 31 |
-
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
| 32 |
-
className
|
| 33 |
-
)}
|
| 34 |
-
{...props}
|
| 35 |
-
>
|
| 36 |
-
<ToggleGroupContext.Provider value={{ variant, size }}>
|
| 37 |
-
{children}
|
| 38 |
-
</ToggleGroupContext.Provider>
|
| 39 |
-
</ToggleGroupPrimitive.Root>
|
| 40 |
-
)
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
function ToggleGroupItem({
|
| 44 |
-
className,
|
| 45 |
-
children,
|
| 46 |
-
variant,
|
| 47 |
-
size,
|
| 48 |
-
...props
|
| 49 |
-
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
| 50 |
-
VariantProps<typeof toggleVariants>) {
|
| 51 |
-
const context = React.useContext(ToggleGroupContext)
|
| 52 |
-
|
| 53 |
-
return (
|
| 54 |
-
<ToggleGroupPrimitive.Item
|
| 55 |
-
data-slot="toggle-group-item"
|
| 56 |
-
data-variant={context.variant || variant}
|
| 57 |
-
data-size={context.size || size}
|
| 58 |
-
className={cn(
|
| 59 |
-
toggleVariants({
|
| 60 |
-
variant: context.variant || variant,
|
| 61 |
-
size: context.size || size,
|
| 62 |
-
}),
|
| 63 |
-
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
| 64 |
-
className
|
| 65 |
-
)}
|
| 66 |
-
{...props}
|
| 67 |
-
>
|
| 68 |
-
{children}
|
| 69 |
-
</ToggleGroupPrimitive.Item>
|
| 70 |
-
)
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
export { ToggleGroup, ToggleGroupItem }
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
| 5 |
+
import { type VariantProps } from "class-variance-authority"
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
import { toggleVariants } from "@/components/ui/toggle"
|
| 9 |
+
|
| 10 |
+
const ToggleGroupContext = React.createContext<
|
| 11 |
+
VariantProps<typeof toggleVariants>
|
| 12 |
+
>({
|
| 13 |
+
size: "default",
|
| 14 |
+
variant: "default",
|
| 15 |
+
})
|
| 16 |
+
|
| 17 |
+
function ToggleGroup({
|
| 18 |
+
className,
|
| 19 |
+
variant,
|
| 20 |
+
size,
|
| 21 |
+
children,
|
| 22 |
+
...props
|
| 23 |
+
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
| 24 |
+
VariantProps<typeof toggleVariants>) {
|
| 25 |
+
return (
|
| 26 |
+
<ToggleGroupPrimitive.Root
|
| 27 |
+
data-slot="toggle-group"
|
| 28 |
+
data-variant={variant}
|
| 29 |
+
data-size={size}
|
| 30 |
+
className={cn(
|
| 31 |
+
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
| 32 |
+
className
|
| 33 |
+
)}
|
| 34 |
+
{...props}
|
| 35 |
+
>
|
| 36 |
+
<ToggleGroupContext.Provider value={{ variant, size }}>
|
| 37 |
+
{children}
|
| 38 |
+
</ToggleGroupContext.Provider>
|
| 39 |
+
</ToggleGroupPrimitive.Root>
|
| 40 |
+
)
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
function ToggleGroupItem({
|
| 44 |
+
className,
|
| 45 |
+
children,
|
| 46 |
+
variant,
|
| 47 |
+
size,
|
| 48 |
+
...props
|
| 49 |
+
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
| 50 |
+
VariantProps<typeof toggleVariants>) {
|
| 51 |
+
const context = React.useContext(ToggleGroupContext)
|
| 52 |
+
|
| 53 |
+
return (
|
| 54 |
+
<ToggleGroupPrimitive.Item
|
| 55 |
+
data-slot="toggle-group-item"
|
| 56 |
+
data-variant={context.variant || variant}
|
| 57 |
+
data-size={context.size || size}
|
| 58 |
+
className={cn(
|
| 59 |
+
toggleVariants({
|
| 60 |
+
variant: context.variant || variant,
|
| 61 |
+
size: context.size || size,
|
| 62 |
+
}),
|
| 63 |
+
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
| 64 |
+
className
|
| 65 |
+
)}
|
| 66 |
+
{...props}
|
| 67 |
+
>
|
| 68 |
+
{children}
|
| 69 |
+
</ToggleGroupPrimitive.Item>
|
| 70 |
+
)
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
export { ToggleGroup, ToggleGroupItem }
|
src/lib/db.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
| 1 |
-
import Dexie, { type EntityTable } from 'dexie';
|
| 2 |
-
|
| 3 |
-
export interface ImageRecord {
|
| 4 |
-
filename: string;
|
| 5 |
-
blob: Blob;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
export class ImageDB extends Dexie {
|
| 9 |
-
images!: EntityTable<ImageRecord, 'filename'>;
|
| 10 |
-
|
| 11 |
-
constructor() {
|
| 12 |
-
super('ImageDB');
|
| 13 |
-
|
| 14 |
-
this.version(1).stores({
|
| 15 |
-
images: '&filename'
|
| 16 |
-
});
|
| 17 |
-
|
| 18 |
-
this.images = this.table('images');
|
| 19 |
-
}
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
export const db = new ImageDB();
|
|
|
|
| 1 |
+
import Dexie, { type EntityTable } from 'dexie';
|
| 2 |
+
|
| 3 |
+
export interface ImageRecord {
|
| 4 |
+
filename: string;
|
| 5 |
+
blob: Blob;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export class ImageDB extends Dexie {
|
| 9 |
+
images!: EntityTable<ImageRecord, 'filename'>;
|
| 10 |
+
|
| 11 |
+
constructor() {
|
| 12 |
+
super('ImageDB');
|
| 13 |
+
|
| 14 |
+
this.version(1).stores({
|
| 15 |
+
images: '&filename'
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
this.images = this.table('images');
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export const db = new ImageDB();
|
src/lib/utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
import { clsx, type ClassValue } from 'clsx';
|
| 2 |
-
import { twMerge } from 'tailwind-merge';
|
| 3 |
-
|
| 4 |
-
export function cn(...inputs: ClassValue[]) {
|
| 5 |
-
return twMerge(clsx(inputs));
|
| 6 |
-
}
|
|
|
|
| 1 |
+
import { clsx, type ClassValue } from 'clsx';
|
| 2 |
+
import { twMerge } from 'tailwind-merge';
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) {
|
| 5 |
+
return twMerge(clsx(inputs));
|
| 6 |
+
}
|
tsconfig.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
| 1 |
-
{
|
| 2 |
-
"compilerOptions": {
|
| 3 |
-
"target": "ES2017",
|
| 4 |
-
"lib": [
|
| 5 |
-
"dom",
|
| 6 |
-
"dom.iterable",
|
| 7 |
-
"esnext"
|
| 8 |
-
],
|
| 9 |
-
"allowJs": true,
|
| 10 |
-
"skipLibCheck": true,
|
| 11 |
-
"strict": true,
|
| 12 |
-
"noEmit": true,
|
| 13 |
-
"esModuleInterop": true,
|
| 14 |
-
"module": "esnext",
|
| 15 |
-
"moduleResolution": "bundler",
|
| 16 |
-
"resolveJsonModule": true,
|
| 17 |
-
"isolatedModules": true,
|
| 18 |
-
"jsx": "react-jsx",
|
| 19 |
-
"incremental": true,
|
| 20 |
-
"plugins": [
|
| 21 |
-
{
|
| 22 |
-
"name": "next"
|
| 23 |
-
}
|
| 24 |
-
],
|
| 25 |
-
"paths": {
|
| 26 |
-
"@/*": [
|
| 27 |
-
"./src/*"
|
| 28 |
-
]
|
| 29 |
-
}
|
| 30 |
-
},
|
| 31 |
-
"include": [
|
| 32 |
-
"next-env.d.ts",
|
| 33 |
-
"**/*.ts",
|
| 34 |
-
"**/*.tsx",
|
| 35 |
-
".next/types/**/*.ts",
|
| 36 |
-
".next/dev/types/**/*.ts"
|
| 37 |
-
],
|
| 38 |
"exclude": [
|
| 39 |
"node_modules",
|
| 40 |
"dist"
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2017",
|
| 4 |
+
"lib": [
|
| 5 |
+
"dom",
|
| 6 |
+
"dom.iterable",
|
| 7 |
+
"esnext"
|
| 8 |
+
],
|
| 9 |
+
"allowJs": true,
|
| 10 |
+
"skipLibCheck": true,
|
| 11 |
+
"strict": true,
|
| 12 |
+
"noEmit": true,
|
| 13 |
+
"esModuleInterop": true,
|
| 14 |
+
"module": "esnext",
|
| 15 |
+
"moduleResolution": "bundler",
|
| 16 |
+
"resolveJsonModule": true,
|
| 17 |
+
"isolatedModules": true,
|
| 18 |
+
"jsx": "react-jsx",
|
| 19 |
+
"incremental": true,
|
| 20 |
+
"plugins": [
|
| 21 |
+
{
|
| 22 |
+
"name": "next"
|
| 23 |
+
}
|
| 24 |
+
],
|
| 25 |
+
"paths": {
|
| 26 |
+
"@/*": [
|
| 27 |
+
"./src/*"
|
| 28 |
+
]
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"include": [
|
| 32 |
+
"next-env.d.ts",
|
| 33 |
+
"**/*.ts",
|
| 34 |
+
"**/*.tsx",
|
| 35 |
+
".next/types/**/*.ts",
|
| 36 |
+
".next/dev/types/**/*.ts"
|
| 37 |
+
],
|
| 38 |
"exclude": [
|
| 39 |
"node_modules",
|
| 40 |
"dist"
|