+ {files.length === 0 ? (
+
+
+
+ {isLoadingFiles ? (
+
+ ) : (
+
暂无文件
+ )}
+
+ ) : (
+ <>{view === "List" ? renderListView() : renderGridView()}>
+ )}
+
+ );
+}
diff --git a/components/file/upload-pending.tsx b/components/file/upload-pending.tsx
new file mode 100644
index 0000000..e2a798b
--- /dev/null
+++ b/components/file/upload-pending.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+
+import { cn, formatFileSize } from "@/lib/utils";
+import { BucketInfo } from "@/components/file/file-list";
+
+import { CopyButton } from "../shared/copy-button";
+import { Icons } from "../shared/icons";
+import { Button } from "../ui/button";
+import { UploadPendingItemType, UploadProgressType } from "./uploader";
+
+const UploadPending = ({
+ pendingUpload,
+ progressList,
+ bucketInfo,
+ onAbort,
+}: {
+ pendingUpload: UploadPendingItemType[] | null;
+ progressList: UploadProgressType[] | undefined;
+ bucketInfo: BucketInfo;
+ onAbort: (uploadId: string, key: string) => void;
+}) => {
+ const t = useTranslations("Components");
+ return (
+
+ {pendingUpload &&
{t("Upload List")}
}
+ {pendingUpload &&
+ pendingUpload.map((item) => {
+ const progress =
+ progressList?.find((p) => p.id === item.uploadId)?.progress || 0;
+ return (
+
+ {/* 主进度条背景 */}
+ {item.status === "uploading" && (
+
+ )}
+
+ {/* 内容区域 */}
+
+ {/* 头部信息 */}
+
+
+
+ {item.fileName}
+
+
+ {formatFileSize(item.size)}
+
+
+
+ {/* 状态指示器 */}
+
+ {item.status === "uploading" ? (
+
+
{progress}%
+
+
+
+ ) : item.status === "completed" ? (
+
+ ) : (
+ item.status === "aborted" && (
+
+ )
+ )}
+
+
+
+ {/* 进度条 */}
+ {item.status === "uploading" && (
+
+
+ {/* 进度填充 */}
+
+ {/* 动态光泽效果 */}
+
0 && progress < 100 ? 1 : 0,
+ }}
+ />
+
+
+ )}
+
+
+ );
+ })}
+
+ );
+};
+
+export default UploadPending;
diff --git a/components/file/uploader.tsx b/components/file/uploader.tsx
new file mode 100644
index 0000000..339f3cb
--- /dev/null
+++ b/components/file/uploader.tsx
@@ -0,0 +1,301 @@
+"use client";
+
+import { useCallback, useEffect, useState } from "react";
+import { useTranslations } from "next-intl";
+
+import { BucketInfo } from "@/components/file/file-list";
+
+import { Icons } from "../shared/icons";
+import { Button } from "../ui/button";
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+} from "../ui/drawer";
+import DragAndDrop from "./drag-and-drop";
+import UploadPending from "./upload-pending";
+
+export type UploadPendingItemType = {
+ uploadId: string;
+ fileName: string;
+ size: number;
+ status: "uploading" | "completed" | "aborted";
+ key: string;
+ path?: string;
+};
+
+export type UploadProgressType = {
+ id: string;
+ progress: number;
+};
+
+export default function Uploader({
+ bucketInfo,
+ action,
+}: {
+ bucketInfo: BucketInfo;
+ action: string;
+}) {
+ const t = useTranslations("Components");
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedFile, setSelectedFile] = useState
(null);
+ const [pendingUpload, setPendingUpload] = useState<
+ UploadPendingItemType[] | null
+ >(null);
+ const [progressList, setProgressList] = useState();
+
+ // Handles the upload process for selected files
+ const handleUpload = useCallback(() => {
+ selectedFile?.forEach((file: File) => {
+ uploadFile(file);
+ setSelectedFile((prev) => (prev ? prev.filter((f) => f !== file) : null));
+ });
+ }, [selectedFile]);
+
+ // Starts the multipart upload process
+ const startUpload = async (
+ file: File,
+ ): Promise<{ uploadId: string; key: string }> => {
+ const formData = new FormData();
+ formData.append("fileName", file.name);
+ formData.append("fileType", file.type);
+ formData.append("bucket", bucketInfo.bucket);
+ formData.append("endPoint", "create-multipart-upload");
+ const response = await fetch(`${action}/r2/uploads`, {
+ method: "POST",
+ body: formData,
+ });
+ const data = await response.json();
+ if (data.error) {
+ throw new Error(data.error);
+ }
+
+ return data; // { uploadId, key }
+ };
+
+ // Uploads file parts in chunks
+ const uploadParts = async (
+ file: File,
+ uploadId: string,
+ key: string,
+ onProgress: (progress: number) => void,
+ ): Promise<{ ETag: string; PartNumber: number }[]> => {
+ const chunkSize = 5 * 1024 * 1024; // 5MB for each chunk
+ const totalChunks = Math.ceil(file.size / chunkSize);
+ const parts: { ETag: string; PartNumber: number }[] = [];
+
+ for (let i = 0; i < totalChunks; i++) {
+ const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
+ const partNumber = i + 1;
+
+ const formData = new FormData();
+ formData.append("chunk", chunk);
+ formData.append("uploadId", uploadId);
+ formData.append("key", key);
+ formData.append("bucket", bucketInfo.bucket);
+ formData.append("partNumber", partNumber.toString());
+ formData.append("endPoint", "upload-part");
+ const uploadResponse = await fetch(`${action}/r2/uploads`, {
+ method: "POST",
+ body: formData,
+ });
+
+ const responseData = await uploadResponse.json();
+
+ if (responseData.error) {
+ throw new Error(responseData.error);
+ }
+
+ if (responseData.etag) {
+ parts.push({ ETag: responseData.etag, PartNumber: partNumber });
+ }
+
+ const progress = Math.round((partNumber / totalChunks) * 100);
+ onProgress(progress);
+ }
+
+ return parts;
+ };
+
+ // Completes the multipart upload process
+ const completeUpload = async (
+ uploadId: string,
+ key: string,
+ parts: { ETag: string; PartNumber: number }[],
+ ): Promise<{ Location: string }> => {
+ const formData = new FormData();
+
+ formData.append("key", key);
+ formData.append("uploadId", uploadId);
+ formData.append("bucket", bucketInfo.bucket);
+ formData.append("parts", JSON.stringify(parts));
+ formData.append("endPoint", "complete-multipart-upload");
+ const response = await fetch(`${action}/r2/uploads`, {
+ method: "POST",
+ body: formData,
+ });
+ const data = await response.json();
+ if (data.error) {
+ throw new Error(data.error);
+ }
+ return data;
+ };
+
+ const abortUpload = async (uploadId: string, key: string) => {
+ const formData = new FormData();
+ formData.append("uploadId", uploadId);
+ formData.append("key", key);
+ formData.append("bucket", bucketInfo.bucket);
+ formData.append("endPoint", "abort-multipart-upload");
+ const response = await fetch(`${action}/r2/uploads`, {
+ method: "POST",
+ body: formData,
+ });
+ const data = await response.json();
+ if (data.error) {
+ throw new Error(data.error);
+ }
+ setPendingUpload(
+ (prev) =>
+ prev?.map((item) =>
+ item.uploadId === uploadId
+ ? { ...item, status: "aborted", path: "" }
+ : item,
+ ) ?? [],
+ );
+ return data;
+ };
+
+ // Manages the entire file upload process
+ const uploadFile = async (file: File): Promise => {
+ try {
+ const { uploadId, key } = await startUpload(file);
+
+ setProgressList((prev) => [
+ ...(prev ?? []),
+ { id: uploadId, progress: 0 },
+ ]);
+
+ setPendingUpload((prev) => [
+ ...(prev ?? []),
+ {
+ uploadId,
+ fileName: file.name,
+ size: file.size,
+ path: "",
+ status: "uploading",
+ key,
+ },
+ ]);
+ const parts = await uploadParts(file, uploadId, key, (progress) => {
+ // console.log(`Upload Progress: ${progress}%`);
+ setProgressList(
+ (prev) =>
+ prev?.map((item) =>
+ item.id === uploadId ? { ...item, progress } : item,
+ ) ?? [],
+ );
+ });
+
+ const result = await completeUpload(uploadId, key, parts);
+
+ setProgressList(
+ (prev) =>
+ prev?.map((item) =>
+ item.id === uploadId ? { ...item, progress: 100 } : item,
+ ) ?? [],
+ );
+
+ setPendingUpload(
+ (prev) =>
+ prev?.map((item) =>
+ item.uploadId === uploadId
+ ? { ...item, status: "completed", path: result.Location }
+ : item,
+ ) ?? [],
+ );
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ // Triggers the upload process when files are selected
+ useEffect(() => {
+ if (selectedFile?.length) {
+ handleUpload();
+ }
+ }, [selectedFile]);
+
+ return (
+ <>
+ {!isOpen && (
+
+ )}
+ {isOpen && (
+
+
+
+
+ {t("Upload Files")}
+
+
+
+
+
+
+
+
{t("Uploud channel")}:
+
{bucketInfo.provider_name}
+
+
+ {bucketInfo.bucket}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/components/forms/domain-form.tsx b/components/forms/domain-form.tsx
index 1123cd4..d3314c3 100644
--- a/components/forms/domain-form.tsx
+++ b/components/forms/domain-form.tsx
@@ -399,7 +399,7 @@ export function DomainForm({
- {t("How to get api token?")}
+ {t("How to get api key?")}
)}
diff --git a/components/shared/file-manager.tsx b/components/shared/file-manager.tsx
deleted file mode 100644
index 09d5290..0000000
--- a/components/shared/file-manager.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-"use client";
-
-import React, {
- ChangeEvent,
- FormEvent,
- useEffect,
- useRef,
- useState,
-} from "react";
-import useSWR from "swr";
-
-import { ClientStorageCredentials, FileObject } from "@/lib/r2";
-import { fetcher } from "@/lib/utils";
-
-export default function FileManager() {
- const [files, setFiles] = useState
([]);
- const [file, setFile] = useState(null);
- const [uploadProgress, setUploadProgress] = useState(0);
- const [isUploading, setIsUploading] = useState(false);
- const abortControllerRef = useRef(null);
- const [currentBucket, setCurrentBucket] = useState("");
-
- const { data: configs, isLoading } = useSWR(
- "/api/s3/r2/configs",
- fetcher,
- );
-
- useEffect(() => {
- if (configs && configs.buckets && configs.buckets.length > 0) {
- setCurrentBucket(configs.buckets[0]);
- }
- if (currentBucket) {
- fetchFiles();
- }
- }, [configs, currentBucket]);
-
- const fetchFiles = async () => {
- try {
- const response = await fetch(`/api/s3/r2/files?bucket=${currentBucket}`);
- const data = await response.json();
- setFiles(Array.isArray(data) ? data : []);
- } catch (error) {
- console.error("Error fetching files:", error);
- setFiles([]);
- }
- };
-
- const handleFileChange = (e: ChangeEvent) => {
- if (e.target.files) {
- setFile(e.target.files[0]);
- }
- };
-
- const handleUpload = async (e: FormEvent) => {
- e.preventDefault();
- if (!file) return;
-
- setIsUploading(true);
- setUploadProgress(0);
- abortControllerRef.current = new AbortController();
-
- try {
- const response = await fetch("/api/s3/r2/upload", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- fileName: file.name,
- fileType: file.type,
- bucket: currentBucket,
- }),
- });
- const { signedUrl } = await response.json();
-
- await uploadFileWithProgress(
- file,
- signedUrl,
- abortControllerRef.current.signal,
- );
-
- alert("File uploaded successfully!");
- setFile(null); // Clear the file input
- fetchFiles();
- } catch (error) {
- if (error instanceof Error && error.name === "AbortError") {
- console.log("Upload cancelled");
- } else {
- console.error("Error uploading file:", error);
- alert("Error uploading file");
- }
- } finally {
- setIsUploading(false);
- setUploadProgress(0);
- abortControllerRef.current = null;
- }
- };
-
- const uploadFileWithProgress = (
- file: File,
- signedUrl: string,
- signal: AbortSignal,
- ): Promise => {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
-
- xhr.open("PUT", signedUrl);
- xhr.setRequestHeader("Content-Type", file.type);
-
- xhr.upload.onprogress = (event) => {
- if (event.lengthComputable) {
- const percentComplete = (event.loaded / event.total) * 100;
- setUploadProgress(percentComplete);
- }
- };
-
- xhr.onload = () => {
- if (xhr.status === 200) {
- resolve();
- } else {
- reject(new Error(`Upload failed with status ${xhr.status}`));
- }
- };
-
- xhr.onerror = () => {
- reject(new Error("Upload failed"));
- };
-
- xhr.send(file);
-
- signal.addEventListener("abort", () => {
- xhr.abort();
- reject(new Error("Upload cancelled"));
- });
- });
- };
-
- const handleCancelUpload = () => {
- if (abortControllerRef.current) {
- abortControllerRef.current.abort();
- }
- };
-
- const handleDownload = async (key: string) => {
- try {
- const response = await fetch("/api/s3/r2/files", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ key, bucket: currentBucket }),
- });
- const { signedUrl } = await response.json();
- window.open(signedUrl, "_blank");
- } catch (error) {
- console.error("Error downloading file:", error);
- alert("Error downloading file");
- }
- };
-
- const handleDelete = async (key: string) => {
- try {
- await fetch("/api/s3/r2/files", {
- method: "DELETE",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ key, bucket: currentBucket }),
- });
- alert("File deleted successfully!");
- fetchFiles();
- } catch (error) {
- console.error("Error deleting file:", error);
- alert("Error deleting file");
- }
- };
-
- return (
-
-
- Cloudflare R2 with Next.js: Upload, Download, Delete
-
-
Upload File
-
-
- {isUploading && (
-
-
-
-
- {uploadProgress.toFixed(2)}% uploaded
-
-
-
-
- )}
-
-
Files
- {files.length === 0 ? (
-
No files found.
- ) : (
-
- )}
-
- );
-}
diff --git a/components/shared/icons.tsx b/components/shared/icons.tsx
index 6d39694..239b280 100644
--- a/components/shared/icons.tsx
+++ b/components/shared/icons.tsx
@@ -88,7 +88,47 @@ export const Icons = {
camera: Camera,
calendar: Calendar,
crown: Crown,
- cloudUpload: CloudUpload,
+ cloudUpload: ({ ...props }: LucideProps) => (
+
+ ),
+ storage: ({ ...props }: LucideProps) => (
+
+ ),
eye: Eye,
lock: LockKeyhole,
list: List,
diff --git a/config/dashboard.ts b/config/dashboard.ts
index 894d6d2..3b308d6 100644
--- a/config/dashboard.ts
+++ b/config/dashboard.ts
@@ -14,7 +14,7 @@ export const sidebarLinks: SidebarNavItem[] = [
{ href: "/emails", icon: "mail", title: "Emails" },
{
href: "/dashboard/storage",
- icon: "cloudUpload",
+ icon: "storage",
title: "Cloud Storage",
},
],
diff --git a/content/docs/developer/cloudflare.mdx b/content/docs/developer/cloudflare.mdx
index dbbc9f2..7cb34c5 100644
--- a/content/docs/developer/cloudflare.mdx
+++ b/content/docs/developer/cloudflare.mdx
@@ -18,7 +18,7 @@ The `Short URL Service` and `Email Service` require no additional configuration
To enable the `DNS Record Service`, you must complete the `Cloudflare Configs(Optional)` form with the following fields:
- Zone ID
-- API Token
+- API Key
- Email
These fields are used to configure the Cloudflare API. If your domain is hosted through Cloudflare, you can find these details in the Cloudflare dashboard.
@@ -29,9 +29,9 @@ The unique identifier for a domain hosted on Cloudflare, located at:
https://dash.cloudflare.com/[account_id]/[zone_name]
-### API Token
+### API Key
-Visit https://dash.cloudflare.com/profile/api-tokens, and find the Global API Key under the API Tokens section.
+Visit https://dash.cloudflare.com/profile/api-tokens, and find the **Global API** Key under the API Tokens section.
### Email
@@ -39,7 +39,7 @@ Email for registering a Cloudflare account
You can manage domains hosted under different Cloudflare accounts,
-provided the API Token and Email are sourced from the same account.
+provided the API Key and Email are sourced from the same account.
---
@@ -72,11 +72,11 @@ NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME=example.com,example2.com
### CLOUDFLARE_API_KEY
- Description: The API key used to authenticate requests to the Cloudflare API.
-- Where to find: In the Cloudflare dashboard, go to Profile > API Tokens and locate your Global API Key.
+- Where to find: In the Cloudflare dashboard, go to Profile > API Tokens and locate your **Global API Key**.
- Example: 1234567890abcdef1234567890abcdef
- Security Note: Keep this key confidential and never expose it in client-side code.
-> Instructions: Visit https://dash.cloudflare.com/profile/api-tokens, and find the Global API Key under the API Tokens section.
+> Instructions: Visit https://dash.cloudflare.com/profile/api-tokens, and find the **Global API Key** under the API Tokens section.
### CLOUDFLARE_EMAIL
diff --git a/lib/dto/user.ts b/lib/dto/user.ts
index b13855d..e82214f 100644
--- a/lib/dto/user.ts
+++ b/lib/dto/user.ts
@@ -200,10 +200,13 @@ export const updateUser = async (userId: string, data: UpdateUserForm) => {
export const deleteUserById = async (userId: string) => {
try {
- const session = await prisma.user.delete({
+ const session = await prisma.user.update({
where: {
id: userId,
},
+ data: {
+ active: 0,
+ },
});
return session;
} catch (error) {
diff --git a/lib/r2.ts b/lib/r2.ts
index 3985054..83dd69a 100644
--- a/lib/r2.ts
+++ b/lib/r2.ts
@@ -1,14 +1,16 @@
import {
+ AbortMultipartUploadCommand,
+ CompleteMultipartUploadCommand,
+ CreateMultipartUploadCommand,
DeleteObjectCommand,
GetObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
S3Client,
+ UploadPartCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
-import { getMultipleConfigs } from "./dto/system-config";
-
export interface CloudStorageCredentials {
enabled?: boolean;
platform?: string;
diff --git a/lib/utils.ts b/lib/utils.ts
index 596a99f..e86fbed 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -397,3 +397,9 @@ export function verifyPassword(
const hashToVerify = crypto.scryptSync(password, salt, 64).toString("hex");
return hash === hashToVerify;
}
+
+export const formatFileSizeX = (bytes: number) => {
+ if (bytes < 1048576) return (bytes / 1024).toFixed() + " KB";
+ if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + " MB";
+ return (bytes / 1073741824).toFixed(2) + " GB";
+};
diff --git a/locales/en.json b/locales/en.json
index 93c5996..f00b1b7 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -130,7 +130,7 @@
"API Token": "API Token",
"Account Email": "Account Email",
"How to get zone id?": "How to get zone id?",
- "How to get api token?": "How to get api token?",
+ "How to get api key?": "How to get api key?",
"How to get cloudflare account email?": "How to get cloudflare account email?",
"Resend Configs": "Resend Configs",
"Associate with 'Subdomain Service' status": "Associate with 'Subdomain Service' status",
@@ -324,7 +324,21 @@
"Actived": "Active",
"Disabled": "Disabled",
"Expired": "Expired",
- "PasswordProtected": "Password Protected"
+ "PasswordProtected": "Password Protected",
+ "Upload List": "Upload List",
+ "Uploading": "Uploading",
+ "Completed": "Completed",
+ "Aborted": "Aborted",
+ "Drop files to upload them to": "Drop files to upload them to",
+ "Drag and drop file(s) here": "Drag and drop file(s) here",
+ "or": "or",
+ "Browse file(s)": "Browse file(s)",
+ "Cloud Storage": "Cloud Storage",
+ "List and manage cloud storage": "List and manage cloud storage",
+ "Cancel": "Cancel",
+ "Clear": "Clear",
+ "Upload Files": "Upload Files",
+ "Uploud channel": "Uploud channel"
},
"Landing": {
"settings": "Settings",
diff --git a/locales/zh.json b/locales/zh.json
index cb51dcb..9d5b6aa 100644
--- a/locales/zh.json
+++ b/locales/zh.json
@@ -130,7 +130,7 @@
"API Token": "API Token",
"Account Email": "账户邮箱",
"How to get zone id?": "如何获取 Zone ID?",
- "How to get api token?": "如何获取 API Token?",
+ "How to get api key?": "如何获取 API 密钥?",
"How to get cloudflare account email?": "如何获取账户邮箱?",
"Resend Configs": "Resend 配置",
"Associate with 'Subdomain Service' status": "与 '子域名服务' 启用状态关联",
@@ -324,7 +324,21 @@
"Actived": "有效",
"Disabled": "已禁用",
"Expired": "已过期",
- "PasswordProtected": "密码保护"
+ "PasswordProtected": "密码保护",
+ "Upload List": "上传列表",
+ "Uploading": "上传中",
+ "Completed": "已完成",
+ "Aborted": "已中止",
+ "Drop files to upload them to": "将文件上传到",
+ "Drag and drop file(s) here": "将文件拖到此处上传",
+ "or": "或",
+ "Browse file(s)": "浏览本地文件",
+ "Cloud Storage": "云存储",
+ "List and manage cloud storage": "上传和管理云存储文件",
+ "Cancel": "取消",
+ "Clear": "清空",
+ "Upload Files": "上传文件",
+ "Uploud channel": "渠道"
},
"Landing": {
"settings": "设置",
diff --git a/package.json b/package.json
index d3d8370..64186ef 100644
--- a/package.json
+++ b/package.json
@@ -115,6 +115,7 @@
"react-countup": "^6.5.3",
"react-day-picker": "^8.10.1",
"react-dom": "18.3.1",
+ "react-dropzone": "^14.3.8",
"react-email": "2.1.5",
"react-globe.gl": "^2.33.2",
"react-hook-form": "^7.52.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3012d21..bd4da12 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -278,6 +278,9 @@ importers:
react-dom:
specifier: 18.3.1
version: 18.3.1(react@18.3.1)
+ react-dropzone:
+ specifier: ^14.3.8
+ version: 14.3.8(react@18.3.1)
react-email:
specifier: 2.1.5
version: 2.1.5(@opentelemetry/api@1.8.0)(@swc/helpers@0.5.5)(eslint@8.57.0)
@@ -4120,6 +4123,10 @@ packages:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
+ attr-accept@2.2.5:
+ resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==}
+ engines: {node: '>=4'}
+
autoprefixer@10.4.14:
resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -5263,6 +5270,10 @@ packages:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
+ file-selector@2.1.2:
+ resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==}
+ engines: {node: '>= 12'}
+
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
@@ -7088,6 +7099,12 @@ packages:
peerDependencies:
react: ^18.3.1
+ react-dropzone@14.3.8:
+ resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==}
+ engines: {node: '>= 10.13'}
+ peerDependencies:
+ react: '>= 16.8 || 18.0.0'
+
react-email@2.1.5:
resolution: {integrity: sha512-SjGt5XiqNwrC6FT0rAxERj0MC9binUOVZDzspAxcRHpxjZavvePAHvV29uROWNQ1Ha7ssg1sfy4dTQi7bjCXrg==}
engines: {node: '>=18.0.0'}
@@ -13057,6 +13074,8 @@ snapshots:
at-least-node@1.0.0: {}
+ attr-accept@2.2.5: {}
+
autoprefixer@10.4.14(postcss@8.4.38):
dependencies:
browserslist: 4.23.0
@@ -14104,7 +14123,7 @@ snapshots:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0)
- eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
+ eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-react: 7.35.0(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
@@ -14140,8 +14159,8 @@ snapshots:
debug: 4.3.4
enhanced-resolve: 5.15.0
eslint: 8.57.0
- eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
- eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
+ eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+ eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.2
is-core-module: 2.13.1
@@ -14152,7 +14171,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-module-utils@2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
+ eslint-module-utils@2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -14163,7 +14182,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
+ eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.3
@@ -14173,7 +14192,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
+ eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
@@ -14435,6 +14454,10 @@ snapshots:
dependencies:
flat-cache: 3.2.0
+ file-selector@2.1.2:
+ dependencies:
+ tslib: 2.8.1
+
filelist@1.0.4:
dependencies:
minimatch: 5.1.6
@@ -14500,7 +14523,7 @@ snapshots:
framer-motion@10.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@emotion/is-prop-valid': 0.8.8
react: 18.3.1
@@ -16592,6 +16615,13 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
+ react-dropzone@14.3.8(react@18.3.1):
+ dependencies:
+ attr-accept: 2.2.5
+ file-selector: 2.1.2
+ prop-types: 15.8.1
+ react: 18.3.1
+
react-email@2.1.5(@opentelemetry/api@1.8.0)(@swc/helpers@0.5.5)(eslint@8.57.0):
dependencies:
'@babel/core': 7.24.5
@@ -16681,7 +16711,7 @@ snapshots:
dependencies:
react: 18.3.1
react-style-singleton: 2.2.1(@types/react@18.2.47)(react@18.3.1)
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.2.47
@@ -16689,7 +16719,7 @@ snapshots:
dependencies:
react: 18.3.1
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -16739,7 +16769,7 @@ snapshots:
get-nonce: 1.0.1
invariant: 2.2.4
react: 18.3.1
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.2.47
@@ -16748,7 +16778,7 @@ snapshots:
get-nonce: 1.0.1
invariant: 2.2.4
react: 18.3.1
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -17844,14 +17874,14 @@ snapshots:
use-callback-ref@1.3.0(@types/react@18.2.47)(react@18.3.1):
dependencies:
react: 18.3.1
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.2.47
use-callback-ref@1.3.0(@types/react@18.3.3)(react@18.3.1):
dependencies:
react: 18.3.1
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -17883,7 +17913,7 @@ snapshots:
dependencies:
detect-node-es: 1.1.0
react: 18.3.1
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.2.47
@@ -17891,7 +17921,7 @@ snapshots:
dependencies:
detect-node-es: 1.1.0
react: 18.3.1
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
diff --git a/public/sw.js.map b/public/sw.js.map
index cc7c1d9..fe5d752 100644
--- a/public/sw.js.map
+++ b/public/sw.js.map
@@ -1 +1 @@
-{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/00e4cad826a34a9a927f45ba2035b590/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/d830702919f7d7cb2216bd72b3811a30/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"}
\ No newline at end of file