diff --git a/package.json b/package.json index 02ae477d3..118646510 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "discord-oauth2": "^2.11.0", "discord.js": "^14.14.1", "fuse.js": "^7.0.0", + "embla-carousel-react": "^8.0.0", "jose": "^5.2.2", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.321.0", diff --git a/prisma/migrations/20240324133414_modify_video_progress/migration.sql b/prisma/migrations/20240324133414_modify_video_progress/migration.sql new file mode 100644 index 000000000..7b93a9771 --- /dev/null +++ b/prisma/migrations/20240324133414_modify_video_progress/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "VideoProgress" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b0215f596..a4bc79d74 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -131,7 +131,7 @@ model User { bookmarks Bookmark[] password String? appxUserId String? - appxUsername String? + appxUsername String? } model DiscordConnect { @@ -150,13 +150,14 @@ model DiscordConnectBulk { } model VideoProgress { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) userId String contentId Int currentTimestamp Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - content Content @relation(fields: [contentId], references: [id], onDelete: Cascade) - markAsCompleted Boolean @default(false) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + content Content @relation(fields: [contentId], references: [id], onDelete: Cascade) + markAsCompleted Boolean @default(false) + updatedAt DateTime @default(now()) @updatedAt @@unique([contentId, userId]) } diff --git a/src/app/api/course/videoProgress/route.ts b/src/app/api/course/videoProgress/route.ts index aa2b643b9..15f3ffb14 100644 --- a/src/app/api/course/videoProgress/route.ts +++ b/src/app/api/course/videoProgress/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import db from '@/db'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; +import { revalidatePath } from 'next/cache'; export async function GET(req: NextRequest) { const url = new URL(req.url); @@ -47,5 +48,6 @@ export async function POST(req: NextRequest) { currentTimestamp, }, }); + revalidatePath('/history'); return NextResponse.json(updatedRecord); } diff --git a/src/app/history/loading.tsx b/src/app/history/loading.tsx new file mode 100644 index 000000000..467c3a68b --- /dev/null +++ b/src/app/history/loading.tsx @@ -0,0 +1,11 @@ +import { CourseSkeleton } from '@/components/CourseCard'; + +export default function Loading() { + return ( +
+ {[1, 2, 3].map((v) => ( + + ))} +
+ ); +} diff --git a/src/app/history/page.tsx b/src/app/history/page.tsx new file mode 100644 index 000000000..e04e2e689 --- /dev/null +++ b/src/app/history/page.tsx @@ -0,0 +1,107 @@ +import db from '@/db'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { Content, CourseContent, VideoProgress } from '@prisma/client'; +import WatchHistoryClient from '@/components/WatchHistoryClient'; +import { Fragment } from 'react'; + +export type TWatchHistory = VideoProgress & { + content: Content & { + parent: { id: number; courses: CourseContent[] } | null; + VideoMetadata: { duration: number | null } | null; + }; +}; + +const formatWatchHistoryDate = (date: Date) => { + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const diffInDays = diff / (1000 * 60 * 60 * 24); + + if (diffInDays < 1) { + return 'Today'; + } else if (diffInDays < 2) { + return 'Yesterday'; + } + const currentYear = now.getFullYear(); + + const historyYear = date.getFullYear(); + + if (currentYear - historyYear > 0) { + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + } + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); +}; + +const groupByWatchedDate = (userVideoProgress: TWatchHistory[]) => { + return userVideoProgress.reduce( + (acc, item) => { + const date = new Date(item.updatedAt); + const formattedDate = formatWatchHistoryDate(date); + + if (!acc[formattedDate]) { + acc[formattedDate] = []; + } + acc[formattedDate].push(item); + return acc; + }, + {} as { [key: string]: TWatchHistory[] }, + ); +}; + +async function getWatchHistory() { + const session = await getServerSession(authOptions); + const userId = session.user.id; + + const userVideoProgress: TWatchHistory[] = await db.videoProgress.findMany({ + where: { + userId, + }, + include: { + content: { + include: { + VideoMetadata: { + select: { + duration: true, + }, + }, + parent: { + select: { + id: true, + courses: true, + }, + }, + }, + }, + }, + orderBy: { + updatedAt: 'desc', + }, + }); + + return userVideoProgress; +} + +export default async function CoursesComponent() { + const watchHistory = await getWatchHistory(); + const watchHistoryGroupedByDate = groupByWatchedDate(watchHistory); + + return ( +
+

Watch history

+
+ {Object.entries(watchHistoryGroupedByDate).map(([date, history]) => { + return ( + +

{date}

+ +
+ ); + })} +
+
+ ); +} diff --git a/src/components/Appbar.tsx b/src/components/Appbar.tsx index f283b2495..723121f94 100644 --- a/src/components/Appbar.tsx +++ b/src/components/Appbar.tsx @@ -48,56 +48,57 @@ export const Appbar = () => {
{/* Search Bar for smaller devices */} -
- -
-
- {currentPath.includes('courses') && bookmarkPageUrl && ( - - )} +
+
+ {currentPath.includes('courses') && bookmarkPageUrl && ( + + )} - + - + - - - -
+ + + +
- + -
- +
+ +
diff --git a/src/components/ContentCard.tsx b/src/components/ContentCard.tsx index a3e19b78a..007b2e10e 100644 --- a/src/components/ContentCard.tsx +++ b/src/components/ContentCard.tsx @@ -9,6 +9,8 @@ export const ContentCard = ({ markAsCompleted, percentComplete, type, + videoProgressPercent, + hoverExpand = true, bookmark, contentId, }: { @@ -19,6 +21,8 @@ export const ContentCard = ({ onClick: () => void; markAsCompleted?: boolean; percentComplete?: number | null; + videoProgressPercent?: number; + hoverExpand?: boolean; bookmark?: Bookmark | null; }) => { let image = @@ -31,7 +35,7 @@ export const ContentCard = ({ return (
{percentComplete !== null && percentComplete !== undefined && ( @@ -41,6 +45,17 @@ export const ContentCard = ({
)} +
+ {title} + {!!videoProgressPercent && ( +
+
+
+ )} +
{bookmark !== undefined && contentId && (
)} - - {title}
{title}
diff --git a/src/components/CourseView.tsx b/src/components/CourseView.tsx index 8b6449917..345705e97 100644 --- a/src/components/CourseView.tsx +++ b/src/components/CourseView.tsx @@ -80,6 +80,8 @@ export const CourseView = ({ id: x?.id || 0, markAsCompleted: x?.videoProgress?.markAsCompleted || false, percentComplete: getFolderPercentCompleted(x?.children), + videoFullDuration: x?.videoProgress?.videoFullDuration || 0, + duration: x?.videoProgress?.duration || 0, }))} courseId={parseInt(course.id, 10)} /> diff --git a/src/components/FolderView.tsx b/src/components/FolderView.tsx index f3bc57041..ec8e6be2a 100644 --- a/src/components/FolderView.tsx +++ b/src/components/FolderView.tsx @@ -17,6 +17,8 @@ export const FolderView = ({ id: number; markAsCompleted: boolean; percentComplete: number | null; + videoFullDuration?: number; + duration?: number; bookmark: Bookmark | null; }[]; }) => { @@ -39,20 +41,29 @@ export const FolderView = ({
- {courseContent.map((content) => ( - { - router.push(`${updatedRoute}/${content.id}`); - }} - markAsCompleted={content.markAsCompleted} - percentComplete={content.percentComplete} - bookmark={content.bookmark} - /> - ))} + {courseContent.map((content) => { + const videoProgressPercent = + content.type === 'video' && + content.videoFullDuration && + content.duration + ? (content.duration / content.videoFullDuration) * 100 + : 0; + return ( + { + router.push(`${updatedRoute}/${content.id}`); + }} + markAsCompleted={content.markAsCompleted} + percentComplete={content.percentComplete} + videoProgressPercent={videoProgressPercent} + bookmark={content.bookmark} + /> + ); + })}
); diff --git a/src/components/VideoPlayer2.tsx b/src/components/VideoPlayer2.tsx index d63a35d17..b14d2bfa1 100644 --- a/src/components/VideoPlayer2.tsx +++ b/src/components/VideoPlayer2.tsx @@ -221,6 +221,9 @@ export const VideoPlayer: FunctionComponent = ({ } interval = window.setInterval( async () => { + if (!player) { + return; + } if (player?.paused()) { return; } diff --git a/src/components/WatchHistoryClient.tsx b/src/components/WatchHistoryClient.tsx new file mode 100644 index 000000000..a1d35a2e6 --- /dev/null +++ b/src/components/WatchHistoryClient.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { ContentCard } from '@/components/ContentCard'; +import { TWatchHistory } from '../app/history/page'; +import { useRouter } from 'next/navigation'; +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, +} from '@/components/ui/carousel'; + +const WatchHistoryClient = ({ history }: { history: TWatchHistory[] }) => ( + + + {history.map((progress) => ( + + + + ))} + + + +); + +const HistoryCard = ({ + id, + contentId, + currentTimestamp, + content: { type, title, thumbnail, hidden, parent, VideoMetadata }, +}: TWatchHistory) => { + const router = useRouter(); + + if (parent && !hidden && type === 'video' && VideoMetadata) { + const { duration: videoDuration } = VideoMetadata; + const { id: folderId, courses } = parent; + const courseId = courses[0].courseId; + const videoUrl = `/courses/${courseId}/${folderId}/${contentId}`; + const videoProgressPercent = videoDuration + ? Math.round((currentTimestamp / videoDuration) * 100) + : 0; + return ( + { + router.push(videoUrl); + }} + videoProgressPercent={videoProgressPercent} + hoverExpand={false} + /> + ); + } +}; + +export default WatchHistoryClient; diff --git a/src/components/admin/ContentRendererClient.tsx b/src/components/admin/ContentRendererClient.tsx index e6a4be66b..d864a5f3a 100644 --- a/src/components/admin/ContentRendererClient.tsx +++ b/src/components/admin/ContentRendererClient.tsx @@ -63,7 +63,7 @@ export const ContentRendererClient = ({ src: mpdUrl, type: 'application/x-mpegURL', }; - } + } return { src: mpdUrl, type: 'video/mp4', diff --git a/src/components/landing/appbar/nav-menu.tsx b/src/components/landing/appbar/nav-menu.tsx index ca0bedf1d..36ebfec5c 100644 --- a/src/components/landing/appbar/nav-menu.tsx +++ b/src/components/landing/appbar/nav-menu.tsx @@ -69,6 +69,12 @@ export function NavigationMenu() { + +
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx new file mode 100644 index 000000000..3943ab96c --- /dev/null +++ b/src/components/ui/carousel.tsx @@ -0,0 +1,262 @@ +'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 '@/components/ui/button'; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +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[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error('useCarousel must be used within a '); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & 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) => { + 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 ( + +
+ {children} +
+
+ ); + }, +); +Carousel.displayName = 'Carousel'; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); +}); +CarouselContent.displayName = 'CarouselContent'; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); +}); +CarouselItem.displayName = 'CarouselItem'; + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); +}); +CarouselPrevious.displayName = 'CarouselPrevious'; + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +}); +CarouselNext.displayName = 'CarouselNext'; + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; diff --git a/src/db/course.ts b/src/db/course.ts index 0e13877dc..b9a69ace6 100644 --- a/src/db/course.ts +++ b/src/db/course.ts @@ -181,10 +181,14 @@ async function getAllContent() { hidden: false, }, include: { + VideoMetadata: { + select: { + duration: true, + }, + }, bookmark: true, }, }); - Cache.getInstance().set('getAllContent', [], allContent); return allContent; @@ -253,6 +257,7 @@ export const getFullCourseContent = async (courseId: number) => { markAsCompleted: videoProgress.find( (x) => x.contentId === content.id, )?.markAsCompleted, + videoFullDuration: content.VideoMetadata?.duration, } : null, }, diff --git a/yarn.lock b/yarn.lock index 33d8960cb..c2f88cda0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -286,11 +286,6 @@ resolved "https://registry.npmjs.org/@icons-pack/react-simple-icons/-/react-simple-icons-9.4.0.tgz" integrity sha512-fZtC4Zv53hE+IQE2dJlFt3EB6UOifwTrUNMuEu4hSXemtqMahd05Dpvj2K0j2ewVc+j/ibavud3xjfaMB2Nj7g== -"@img/sharp-win32-x64@0.33.2": - version "0.33.2" - resolved "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz" - integrity sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg== - "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" @@ -376,10 +371,10 @@ resolved "https://registry.npmjs.org/@next/env/-/env-14.0.2.tgz" integrity sha512-HAW1sljizEaduEOes/m84oUqeIDAUYBR1CDwu2tobNlNDFP3cSm9d6QsOsGeNlIppU1p/p1+bWbYCbvwjFiceA== -"@next/swc-win32-x64-msvc@14.0.2": +"@next/swc-darwin-arm64@14.0.2": version "14.0.2" - resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.2.tgz" - integrity sha512-Ut4LXIUvC5m8pHTe2j0vq/YDnTEyq6RSR9vHYPqnELrDapPhLNz9Od/L5Ow3J8RNDWpEnfCiQXuVdfjlNEJ7ug== + resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.2.tgz" + integrity sha512-i+jQY0fOb8L5gvGvojWyZMfQoQtDVB2kYe7fufOEiST6sicvzI2W5/EXo4lX5bLUjapHKe+nFxuVv7BA+Pd7LQ== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -734,25 +729,6 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" -"@radix-ui/react-tooltip@^1.0.7": - version "1.0.7" - resolved "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz" - integrity sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw== - dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-dismissable-layer" "1.0.5" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-popper" "1.1.3" - "@radix-ui/react-portal" "1.0.4" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-slot" "1.0.2" - "@radix-ui/react-use-controllable-state" "1.0.1" - "@radix-ui/react-visually-hidden" "1.0.3" - "@radix-ui/react-use-callback-ref@1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz" @@ -1644,32 +1620,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@^1.0.0, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.9.0: - version "1.9.1" - resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - color-support@^1.1.2: version "1.1.3" resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -color@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz" - integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== - dependencies: - color-convert "^2.0.1" - color-string "^1.9.0" - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1802,7 +1762,7 @@ delegates@^1.0.0: resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== -detect-libc@^2.0.0, detect-libc@^2.0.2: +detect-libc@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz" integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== @@ -2228,6 +2188,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -2475,11 +2440,6 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" @@ -3839,35 +3799,6 @@ set-harmonic-interval@^1.0.1: resolved "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz" integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g== -sharp@^0.33.2: - version "0.33.2" - resolved "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz" - integrity sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ== - dependencies: - color "^4.2.3" - detect-libc "^2.0.2" - semver "^7.5.4" - optionalDependencies: - "@img/sharp-darwin-arm64" "0.33.2" - "@img/sharp-darwin-x64" "0.33.2" - "@img/sharp-libvips-darwin-arm64" "1.0.1" - "@img/sharp-libvips-darwin-x64" "1.0.1" - "@img/sharp-libvips-linux-arm" "1.0.1" - "@img/sharp-libvips-linux-arm64" "1.0.1" - "@img/sharp-libvips-linux-s390x" "1.0.1" - "@img/sharp-libvips-linux-x64" "1.0.1" - "@img/sharp-libvips-linuxmusl-arm64" "1.0.1" - "@img/sharp-libvips-linuxmusl-x64" "1.0.1" - "@img/sharp-linux-arm" "0.33.2" - "@img/sharp-linux-arm64" "0.33.2" - "@img/sharp-linux-s390x" "0.33.2" - "@img/sharp-linux-x64" "0.33.2" - "@img/sharp-linuxmusl-arm64" "0.33.2" - "@img/sharp-linuxmusl-x64" "0.33.2" - "@img/sharp-wasm32" "0.33.2" - "@img/sharp-win32-ia32" "0.33.2" - "@img/sharp-win32-x64" "0.33.2" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -3890,13 +3821,6 @@ signal-exit@^4.0.1: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"