feat: add image upload functionality for menu items
Release package / release (pull_request) Has been cancelled
Release package / release (pull_request) Has been cancelled
This commit is contained in:
@@ -1,103 +1,10 @@
|
||||
# Coffee Shop Frontend - TODO
|
||||
# TODO - Add image upload field for manager add/update menu item
|
||||
|
||||
## Completed Features & Implementations
|
||||
|
||||
### A. Dead Code Removed
|
||||
|
||||
- [x] lib/constants.ts - Removed unused NAV_LINKS export
|
||||
- [x] lib/types.ts - Removed unused NavLink interface
|
||||
- [x] components/Navbar.tsx - Removed trivial handleClick wrapper; inlined
|
||||
onCategoryChange call
|
||||
- [x] components/Navbar.tsx - Removed unused Link import
|
||||
|
||||
### B. Bugs / Inaccuracies Fixed
|
||||
|
||||
- [x] layouts/header.tsx - Fixed JSDoc: 3-column -> 2-column layout (no center
|
||||
section exists)
|
||||
- [x] app/page.tsx - Added available !== false filter to product list
|
||||
- [x] app/page.tsx - Fixed setState-in-effect lint error: moved initial sidebar
|
||||
state to lazy useState initializer
|
||||
- [x] next.config.ts - Added explanatory JSDoc comment
|
||||
|
||||
### C. Documentation Updated
|
||||
|
||||
- [x] README.md - Fixed file structure tree, removed SCSS, fixed dark mode note,
|
||||
updated tech table
|
||||
- [x] components/COMPONENTS.md - Fixed CartProduct styling (was outdated
|
||||
w-64/text-red-500/bg-blue-600); added Navbar, Header, Footer sections
|
||||
|
||||
### D. New Documentation Created
|
||||
|
||||
- [x] WORKFLOW.md - Architecture, data flow, design token system, how-to guides,
|
||||
dev workflow
|
||||
|
||||
### E. Mini-test Results
|
||||
|
||||
- [x] npm run lint - PASSED (0 errors, 0 warnings)
|
||||
- [x] npm run build - PASSED (Compiled successfully, TypeScript clean, static
|
||||
pages generated)
|
||||
|
||||
---
|
||||
|
||||
## Pending Features (Future Work)
|
||||
|
||||
### Cart & Ordering
|
||||
|
||||
- [ ] Implement cart checkout flow (app/(main)/cart or modal)
|
||||
- [ ] Cart sidebar/modal with item list and total
|
||||
- [ ] Order submission API integration
|
||||
- [ ] Payment page implementation (app/(main)/payment)
|
||||
- [ ] Order history/tracking page
|
||||
- [ ] Toast notifications for cart actions
|
||||
|
||||
### Authentication & User Management
|
||||
|
||||
- [ ] Real backend authentication (replace MOCK_AUTH_DB)
|
||||
- [ ] Real OTP delivery service (SMS integration)
|
||||
- [ ] User profile page with edit capability
|
||||
- [ ] Password reset/recovery flow
|
||||
- [ ] Session management and token refresh
|
||||
|
||||
### Manager Features
|
||||
|
||||
- [ ] Manager dashboard page (app/(manager)/page.tsx)
|
||||
- [ ] Product management (add/edit/delete)
|
||||
- [ ] Category management
|
||||
- [ ] Order management & tracking
|
||||
- [ ] Sales analytics/dashboard
|
||||
- [ ] Inventory management
|
||||
|
||||
### Backend Integration
|
||||
|
||||
- [ ] Replace MOCK_PRODUCTS with API calls (GET /api/products)
|
||||
- [ ] Replace MOCK_SHOPS with API calls (GET /api/shops)
|
||||
- [ ] Replace MOCK_USERS with real authentication (POST /api/auth/login)
|
||||
- [ ] Real product images (replace placeholder.jpg)
|
||||
- [ ] Image upload for products
|
||||
|
||||
### UX Improvements
|
||||
|
||||
- [ ] Dark mode toggle (CSS variables prepared, toggle UI needed)
|
||||
- [ ] Loading skeletons for product grid
|
||||
- [ ] Product detail modal/page with full description
|
||||
- [ ] Wishlist/favorites feature
|
||||
- [ ] Sort products (price, rating, etc.)
|
||||
- [ ] Filter by price range
|
||||
- [ ] Quantity selector in product card
|
||||
- [ ] Related products suggestions
|
||||
|
||||
### Performance & SEO
|
||||
|
||||
- [ ] Dynamic route generation for products (app/(main)/product/[id]/page.tsx)
|
||||
- [ ] Dynamic route generation for shops (app/(feed)/shop/[id]/page.tsx)
|
||||
- [ ] Meta tags and Open Graph for SEO
|
||||
- [ ] Image optimization and lazy loading
|
||||
- [ ] Code splitting and dynamic imports
|
||||
|
||||
### Accessibility & Testing
|
||||
|
||||
- [ ] Keyboard navigation testing
|
||||
- [ ] ARIA labels audit
|
||||
- [ ] Unit tests for contexts
|
||||
- [ ] E2E tests for user flows
|
||||
- [ ] Accessibility audit (WCAG 2.1 AA)
|
||||
- [x] Update GraphQL queries/mutations in `lib/manager-context.tsx` to include `imageUrl`
|
||||
- [x] Update `components/organisms/manager/ProductModal.tsx`
|
||||
- [x] Add file input for image selection
|
||||
- [x] Add upload button to call `POST /api/file`
|
||||
- [x] Store returned URL string into `form.imageUrl`
|
||||
- [x] Show upload/loading/error states and image preview
|
||||
- [x] Update `components/organisms/manager/ProductsTab.tsx` to show image thumbnail in table
|
||||
- [x] Mark TODO progress after each step completed
|
||||
|
||||
@@ -14,13 +14,60 @@ export default function ProductModal({
|
||||
const [form, setForm] = useState<MenuItemEntity>({
|
||||
name: product?.name ?? "",
|
||||
price: product?.price ?? 0,
|
||||
imageUrl: product?.imageUrl ?? "/imgs/products/placeholder.jpg",
|
||||
imageUrl: product?.imageUrl ?? "",
|
||||
description: product?.description ?? "",
|
||||
available: product?.available ?? true,
|
||||
});
|
||||
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
|
||||
const inputCls =
|
||||
"text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20";
|
||||
|
||||
const uploadImage = async (file: File) => {
|
||||
setUploading(true);
|
||||
setUploadError(null);
|
||||
|
||||
try {
|
||||
const body = new FormData();
|
||||
body.append("file", file);
|
||||
|
||||
const res = await fetch("/api/file", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Upload ảnh thất bại.");
|
||||
}
|
||||
|
||||
const raw = await res.text();
|
||||
|
||||
let filename = "";
|
||||
try {
|
||||
const parsed = JSON.parse(raw.trim());
|
||||
filename = parsed.filename || "";
|
||||
} catch {
|
||||
const match = raw.match(/"filename"\s*:\s*"([^"]+)"/i);
|
||||
filename = match?.[1] || "";
|
||||
}
|
||||
|
||||
if (!filename) {
|
||||
throw new Error("Không nhận được filename ảnh từ server.");
|
||||
}
|
||||
|
||||
setForm((prev) => ({ ...prev, imageUrl: filename }));
|
||||
} catch (error: any) {
|
||||
setUploadError(error?.message || "Không thể upload ảnh.");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (isEdit && product) {
|
||||
onSave({ ...form, id: product.id });
|
||||
} else {
|
||||
@@ -28,14 +75,21 @@ export default function ProductModal({
|
||||
}
|
||||
};
|
||||
|
||||
const inputCls =
|
||||
"text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20";
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
await uploadImage(file);
|
||||
};
|
||||
|
||||
const toDisplayUrl = (filename: string) =>
|
||||
filename && !filename.startsWith("/") && !filename.startsWith("http")
|
||||
? `/api/file/${filename}`
|
||||
: filename;
|
||||
|
||||
const previewUrl = toDisplayUrl(form.imageUrl);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4 backdrop-blur-sm"
|
||||
onClick={(e) => e.target === e.currentTarget && onClose()}
|
||||
>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4 backdrop-blur-sm">
|
||||
<div className="w-full max-w-lg rounded-2xl bg-white shadow-2xl">
|
||||
<div className="flex items-center justify-between border-b border-(--color-border-light) px-6 py-4">
|
||||
<h2 className="text-foreground text-lg font-bold">
|
||||
@@ -83,6 +137,46 @@ export default function ProductModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-(--color-text-secondary)">
|
||||
Ảnh món
|
||||
</label>
|
||||
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
disabled={uploading}
|
||||
className="text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm disabled:opacity-60"
|
||||
/>
|
||||
|
||||
{uploading && (
|
||||
<p className="text-sm text-(--color-text-muted)">
|
||||
<i className="fa-solid fa-spinner mr-1 animate-spin"></i>
|
||||
Đang upload ảnh...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{previewUrl && (
|
||||
<div className="mt-2">
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt="Preview ảnh món"
|
||||
className="h-24 w-24 rounded-lg border border-(--color-border-light) object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uploadError && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
<i className="fa-solid fa-circle-exclamation mr-1"></i>
|
||||
{uploadError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-(--color-text-secondary)">
|
||||
Mô tả
|
||||
@@ -131,7 +225,8 @@ export default function ProductModal({
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 cursor-pointer rounded-xl border-none bg-(--color-primary) px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-(--color-primary-dark) active:scale-95"
|
||||
disabled={uploading}
|
||||
className="flex-1 cursor-pointer rounded-xl border-none bg-(--color-primary) px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-(--color-primary-dark) active:scale-95 disabled:opacity-60"
|
||||
>
|
||||
{isEdit ? "Lưu thay đổi" : "Thêm món"}
|
||||
</button>
|
||||
|
||||
@@ -12,6 +12,12 @@ function formatPrice(price: number) {
|
||||
return price.toLocaleString("vi-VN") + "đ";
|
||||
}
|
||||
|
||||
function toDisplayUrl(filename: string) {
|
||||
if (!filename) return "/imgs/products/placeholder.jpg";
|
||||
if (filename.startsWith("/") || filename.startsWith("http")) return filename;
|
||||
return `/api/file/${filename}`;
|
||||
}
|
||||
|
||||
export default function ProductsTab() {
|
||||
const {
|
||||
products,
|
||||
@@ -102,6 +108,9 @@ export default function ProductsTab() {
|
||||
<table className="min-w-full divide-y divide-(--color-border-light) text-sm">
|
||||
<thead className="bg-background">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">
|
||||
Ảnh
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">
|
||||
Tên món
|
||||
</th>
|
||||
@@ -133,6 +142,13 @@ export default function ProductsTab() {
|
||||
key={p.id}
|
||||
className="hover:bg-background transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<img
|
||||
src={toDisplayUrl(p.imageUrl)}
|
||||
alt={p.name}
|
||||
className="h-10 w-10 rounded-lg border border-(--color-border-light) object-cover"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div>
|
||||
<p className="text-foreground font-medium">{p.name}</p>
|
||||
|
||||
+21
-1
@@ -51,6 +51,7 @@ const GET_EATERY_MENU = gql`
|
||||
menuItems {
|
||||
id
|
||||
name
|
||||
imageUrl
|
||||
available
|
||||
description
|
||||
price
|
||||
@@ -64,6 +65,7 @@ const ADD_MENU_ITEM = gql`
|
||||
addMenuItem(menuItem: $menuItem) {
|
||||
id
|
||||
name
|
||||
imageUrl
|
||||
available
|
||||
description
|
||||
price
|
||||
@@ -76,6 +78,7 @@ const UPDATE_MENU_ITEM = gql`
|
||||
updateMenuItem(menuItem: $menuItem) {
|
||||
id
|
||||
name
|
||||
imageUrl
|
||||
available
|
||||
description
|
||||
price
|
||||
@@ -129,7 +132,24 @@ export function ManagerProvider({ children }: { children: ReactNode }) {
|
||||
},
|
||||
});
|
||||
|
||||
if (data) setProducts((prev) => [...prev, data.addMenuItem]);
|
||||
if (!data) return;
|
||||
|
||||
// addMenuItem backend does not persist imageUrl (constructor only maps name+price).
|
||||
// If the caller provided an imageUrl, patch it immediately via updateMenuItem
|
||||
// which uses MapStruct and correctly saves all fields.
|
||||
if (product.imageUrl && data.addMenuItem?.id) {
|
||||
const { data: updated } = await mutateUpdateMenuItem({
|
||||
variables: {
|
||||
menuItem: { id: data.addMenuItem.id, imageUrl: product.imageUrl },
|
||||
},
|
||||
});
|
||||
if (updated) {
|
||||
setProducts((prev) => [...prev, updated.updateMenuItem]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setProducts((prev) => [...prev, data.addMenuItem]);
|
||||
};
|
||||
|
||||
const updateProduct = async (product: MenuItemEntity) => {
|
||||
|
||||
Generated
-19
@@ -164,7 +164,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1429,7 +1428,6 @@
|
||||
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.3",
|
||||
@@ -2429,7 +2427,6 @@
|
||||
"integrity": "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/parser": "^7.28.0",
|
||||
@@ -2565,7 +2562,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -2635,7 +2631,6 @@
|
||||
"integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.57.2",
|
||||
"@typescript-eslint/types": "8.57.2",
|
||||
@@ -3209,7 +3204,6 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3832,7 +3826,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -5340,7 +5333,6 @@
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5526,7 +5518,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -6824,7 +6815,6 @@
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz",
|
||||
"integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
@@ -8523,7 +8513,6 @@
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
@@ -10927,7 +10916,6 @@
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -11750,7 +11738,6 @@
|
||||
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -12065,7 +12052,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -12075,7 +12061,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -12451,7 +12436,6 @@
|
||||
"integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||
"@semantic-release/error": "^4.0.0",
|
||||
@@ -13709,7 +13693,6 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13935,7 +13918,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -14587,7 +14569,6 @@
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user