feat: 배너 이미지 미리보기 다이얼로그 작업
This commit is contained in:
@@ -9,7 +9,9 @@ import { AttachmentApi } from "@/entities/attachment";
|
|||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import type { Banner } from "./banner-interface";
|
import type { Banner } from "./banner-interface";
|
||||||
|
|
||||||
export const columns: ColumnDef<Banner>[] = [
|
export const columns = (
|
||||||
|
imageRefs: React.MutableRefObject<Map<number, string>>,
|
||||||
|
): ColumnDef<Banner>[] => [
|
||||||
{
|
{
|
||||||
id: "select",
|
id: "select",
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
@@ -33,7 +35,13 @@ export const columns: ColumnDef<Banner>[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "배너 이미지",
|
header: "배너 이미지",
|
||||||
cell: ({ row }) => <BannerImage atflCd={row.original.atflCd} />,
|
cell: ({ row }) => (
|
||||||
|
<BannerImage
|
||||||
|
atflCd={row.original.atflCd}
|
||||||
|
bnrSn={row.original.bnrSn}
|
||||||
|
imageRefs={imageRefs}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "bnrTtl",
|
accessorKey: "bnrTtl",
|
||||||
@@ -61,11 +69,23 @@ export const columns: ColumnDef<Banner>[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const BannerImage = ({ atflCd }: { atflCd: string }) => {
|
type BannerImageProps = {
|
||||||
|
atflCd: string;
|
||||||
|
bnrSn: number;
|
||||||
|
imageRefs: React.MutableRefObject<Map<number, string>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BannerImage = ({ atflCd, bnrSn, imageRefs }: BannerImageProps) => {
|
||||||
|
const prevImage = imageRefs.current.get(bnrSn);
|
||||||
|
|
||||||
const { data: imageUrl, isLoading } = useQuery({
|
const { data: imageUrl, isLoading } = useQuery({
|
||||||
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
|
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
|
||||||
queryFn: () => AttachmentApi.getImageUrlByAtflCd(atflCd),
|
queryFn: () => AttachmentApi.getImageUrlByAtflCd(atflCd),
|
||||||
enabled: !!atflCd,
|
select: (url) => {
|
||||||
|
if (url) imageRefs.current.set(bnrSn, url);
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
enabled: !prevImage && !!atflCd,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) return <Skeleton className="h-[4rem] w-[16rem]" />;
|
if (isLoading) return <Skeleton className="h-[4rem] w-[16rem]" />;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +15,7 @@ import { ENDPOINT } from "@/shared/config";
|
|||||||
import { BannerApi } from "../api/banner-service";
|
import { BannerApi } from "../api/banner-service";
|
||||||
import { columns } from "../model/banner-table-columns";
|
import { columns } from "../model/banner-table-columns";
|
||||||
import { type Banner, type BannerParams } from "../model/banner-interface";
|
import { type Banner, type BannerParams } from "../model/banner-interface";
|
||||||
|
import { PreviewBannerDialog } from "./preview-banner-dialog";
|
||||||
|
|
||||||
type PostingTarget = {
|
type PostingTarget = {
|
||||||
pstgTrgtNm: string;
|
pstgTrgtNm: string;
|
||||||
@@ -23,6 +24,7 @@ type PostingTarget = {
|
|||||||
|
|
||||||
function BannerPage() {
|
function BannerPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const imageRefs = useRef<Map<number, string>>(new Map());
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||||
@@ -45,7 +47,7 @@ function BannerPage() {
|
|||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: bannerList?.content ?? [],
|
data: bannerList?.content ?? [],
|
||||||
columns,
|
columns: columns(imageRefs),
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
@@ -67,6 +69,15 @@ function BannerPage() {
|
|||||||
router.push(`/banner/register/${row.bnrSn}`);
|
router.push(`/banner/register/${row.bnrSn}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSelectedBannerImages = () => {
|
||||||
|
const selectedImages: string[] = [];
|
||||||
|
table.getFilteredSelectedRowModel().rows.forEach((row) => {
|
||||||
|
const imageUrl = imageRefs.current.get(row.original.bnrSn);
|
||||||
|
if (imageUrl) selectedImages.push(imageUrl);
|
||||||
|
});
|
||||||
|
return selectedImages;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const postingTargets = table
|
const postingTargets = table
|
||||||
.getFilteredSelectedRowModel()
|
.getFilteredSelectedRowModel()
|
||||||
@@ -115,6 +126,7 @@ function BannerPage() {
|
|||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<h3 className="text-2xl font-medium text-label">배너 목록</h3>
|
<h3 className="text-2xl font-medium text-label">배너 목록</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<PreviewBannerDialog images={getSelectedBannerImages()} />
|
||||||
<PostingDialog
|
<PostingDialog
|
||||||
postingType={POSTING_TYPE.BANNER}
|
postingType={POSTING_TYPE.BANNER}
|
||||||
postingTargets={postingTargets}
|
postingTargets={postingTargets}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
Button,
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselNext,
|
||||||
|
CarouselPrevious,
|
||||||
|
} from "@common/components";
|
||||||
|
|
||||||
|
type PreviewBannerDialogProps = {
|
||||||
|
images: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function PreviewBannerDialog({ images }: PreviewBannerDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button disabled={!images.length}>미리보기</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>배너 이미지 미리보기</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="mt-4 px-12">
|
||||||
|
<Carousel>
|
||||||
|
<CarouselContent>
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<CarouselItem key={index} className="basis-1/3">
|
||||||
|
<div className="relative flex aspect-video items-center justify-center">
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
alt={`배너 이미지 ${index + 1}`}
|
||||||
|
fill
|
||||||
|
objectFit="contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious className="!bg-accent text-accent-foreground" />
|
||||||
|
<CarouselNext className="!bg-accent text-accent-foreground" />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PreviewBannerDialog };
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.474.0",
|
"lucide-react": "^0.474.0",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import useEmblaCarousel, {
|
||||||
|
type UseEmblaCarouselType,
|
||||||
|
} from "embla-carousel-react";
|
||||||
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
import { Button } from "../button";
|
||||||
|
|
||||||
|
type CarouselApi = UseEmblaCarouselType[1];
|
||||||
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||||
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
|
|
||||||
|
type CarouselProps = {
|
||||||
|
opts?: CarouselOptions;
|
||||||
|
plugins?: CarouselPlugin;
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
|
setApi?: (api: CarouselApi) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CarouselContextProps = {
|
||||||
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||||
|
scrollPrev: () => void;
|
||||||
|
scrollNext: () => void;
|
||||||
|
canScrollPrev: boolean;
|
||||||
|
canScrollNext: boolean;
|
||||||
|
} & CarouselProps;
|
||||||
|
|
||||||
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||||
|
|
||||||
|
function useCarousel() {
|
||||||
|
const context = React.useContext(CarouselContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCarousel must be used within a <Carousel />");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Carousel = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
orientation = "horizontal",
|
||||||
|
opts,
|
||||||
|
setApi,
|
||||||
|
plugins,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
axis: orientation === "horizontal" ? "x" : "y",
|
||||||
|
},
|
||||||
|
plugins,
|
||||||
|
);
|
||||||
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||||
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||||
|
|
||||||
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
|
if (!api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanScrollPrev(api.canScrollPrev());
|
||||||
|
setCanScrollNext(api.canScrollNext());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollPrev = React.useCallback(() => {
|
||||||
|
api?.scrollPrev();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const scrollNext = React.useCallback(() => {
|
||||||
|
api?.scrollNext();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
event.preventDefault();
|
||||||
|
scrollPrev();
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
event.preventDefault();
|
||||||
|
scrollNext();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollPrev, scrollNext],
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api || !setApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setApi(api);
|
||||||
|
}, [api, setApi]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(api);
|
||||||
|
api.on("reInit", onSelect);
|
||||||
|
api.on("select", onSelect);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api?.off("select", onSelect);
|
||||||
|
};
|
||||||
|
}, [api, onSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselContext.Provider
|
||||||
|
value={{
|
||||||
|
carouselRef,
|
||||||
|
api: api,
|
||||||
|
opts,
|
||||||
|
orientation:
|
||||||
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||||
|
scrollPrev,
|
||||||
|
scrollNext,
|
||||||
|
canScrollPrev,
|
||||||
|
canScrollNext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
className={cn("relative", className)}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CarouselContext.Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Carousel.displayName = "Carousel";
|
||||||
|
|
||||||
|
const CarouselContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { carouselRef, orientation } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={carouselRef} className="overflow-hidden">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex",
|
||||||
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CarouselContent.displayName = "CarouselContent";
|
||||||
|
|
||||||
|
const CarouselItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CarouselItem.displayName = "CarouselItem";
|
||||||
|
|
||||||
|
const CarouselPrevious = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||||
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute h-8 w-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "-left-12 top-1/2 -translate-y-1/2"
|
||||||
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={!canScrollPrev}
|
||||||
|
onClick={scrollPrev}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Previous slide</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CarouselPrevious.displayName = "CarouselPrevious";
|
||||||
|
|
||||||
|
const CarouselNext = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||||
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute h-8 w-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "-right-12 top-1/2 -translate-y-1/2"
|
||||||
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={!canScrollNext}
|
||||||
|
onClick={scrollNext}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Next slide</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CarouselNext.displayName = "CarouselNext";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselNext,
|
||||||
|
CarouselPrevious,
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ export * from "./accordion";
|
|||||||
export * from "./badge";
|
export * from "./badge";
|
||||||
export * from "./button";
|
export * from "./button";
|
||||||
export * from "./calendar";
|
export * from "./calendar";
|
||||||
|
export * from "./carousel";
|
||||||
export * from "./checkbox";
|
export * from "./checkbox";
|
||||||
export * from "./data-table";
|
export * from "./data-table";
|
||||||
export * from "./date-picker";
|
export * from "./date-picker";
|
||||||
|
|||||||
Generated
+25
@@ -447,6 +447,9 @@ importers:
|
|||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
|
embla-carousel-react:
|
||||||
|
specifier: ^8.6.0
|
||||||
|
version: 8.6.0(react@18.3.1)
|
||||||
js-cookie:
|
js-cookie:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
@@ -4821,6 +4824,28 @@ packages:
|
|||||||
resolution: {integrity: sha512-dJr1o6yCntRkXElnhsHh1bAV19bo/hKyFf7tCcWgpXbuFIF0Lakjgqv5LRfSDaNzAII8Fnxg2tqgHkgCvxdbxw==}
|
resolution: {integrity: sha512-dJr1o6yCntRkXElnhsHh1bAV19bo/hKyFf7tCcWgpXbuFIF0Lakjgqv5LRfSDaNzAII8Fnxg2tqgHkgCvxdbxw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/embla-carousel-react@8.6.0(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.3.1
|
||||||
|
dependencies:
|
||||||
|
embla-carousel: 8.6.0
|
||||||
|
embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0)
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0):
|
||||||
|
resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==}
|
||||||
|
peerDependencies:
|
||||||
|
embla-carousel: 8.6.0
|
||||||
|
dependencies:
|
||||||
|
embla-carousel: 8.6.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/embla-carousel@8.6.0:
|
||||||
|
resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/emoji-regex@8.0.0:
|
/emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user