From 53ca8614b8a6f2bd1dafd97a606f6a0526da3372 Mon Sep 17 00:00:00 2001 From: Ryan Pandya Date: Thu, 14 Nov 2024 23:08:47 -0800 Subject: [PATCH] Labels truly half (or less) working --- .../[tagId] => labels/[labelId]}/page.tsx | 0 apps/web/app/dashboard/labels/page.tsx | 10 + apps/web/app/dashboard/tags/page.tsx | 15 - apps/web/app/page.tsx | 2 +- .../dashboard/labels/AddLabelDialog.tsx | 156 ++++++ .../DeleteTagConfirmationDialog.tsx | 0 .../{tags => labels}/EditableTagName.tsx | 0 .../dashboard/labels/LabelsView.tsx | 84 +++ .../{tags => labels}/MergeTagModal.tsx | 0 .../dashboard/{tags => labels}/TagOptions.tsx | 0 .../dashboard/{tags => labels}/TagPill.tsx | 0 .../{tags => labels}/TagSelector.tsx | 0 .../components/dashboard/sidebar/Sidebar.tsx | 9 +- .../components/dashboard/tags/AllTagsView.tsx | 210 -------- apps/web/package.json | 3 +- .../db/migrations/0005_ambitious_famine.sql | 11 + packages/db/migrations/0006_milky_hellion.sql | 4 + .../db/migrations/meta/0005_snapshot.json | 497 +++++++++++++++++ .../db/migrations/meta/0006_snapshot.json | 498 ++++++++++++++++++ packages/db/migrations/meta/_journal.json | 14 + packages/db/schema.ts | 34 ++ packages/shared/types/labels.ts | 24 + packages/trpc/routers/_app.ts | 2 + packages/trpc/routers/labels.ts | 146 +++++ pnpm-lock.yaml | 20 +- 25 files changed, 1502 insertions(+), 237 deletions(-) rename apps/web/app/dashboard/{tags/[tagId] => labels/[labelId]}/page.tsx (100%) create mode 100644 apps/web/app/dashboard/labels/page.tsx delete mode 100644 apps/web/app/dashboard/tags/page.tsx create mode 100644 apps/web/components/dashboard/labels/AddLabelDialog.tsx rename apps/web/components/dashboard/{tags => labels}/DeleteTagConfirmationDialog.tsx (100%) rename apps/web/components/dashboard/{tags => labels}/EditableTagName.tsx (100%) create mode 100644 apps/web/components/dashboard/labels/LabelsView.tsx rename apps/web/components/dashboard/{tags => labels}/MergeTagModal.tsx (100%) rename apps/web/components/dashboard/{tags => labels}/TagOptions.tsx (100%) rename apps/web/components/dashboard/{tags => labels}/TagPill.tsx (100%) rename apps/web/components/dashboard/{tags => labels}/TagSelector.tsx (100%) delete mode 100644 apps/web/components/dashboard/tags/AllTagsView.tsx create mode 100644 packages/db/migrations/0005_ambitious_famine.sql create mode 100644 packages/db/migrations/0006_milky_hellion.sql create mode 100644 packages/db/migrations/meta/0005_snapshot.json create mode 100644 packages/db/migrations/meta/0006_snapshot.json create mode 100644 packages/shared/types/labels.ts create mode 100644 packages/trpc/routers/labels.ts diff --git a/apps/web/app/dashboard/tags/[tagId]/page.tsx b/apps/web/app/dashboard/labels/[labelId]/page.tsx similarity index 100% rename from apps/web/app/dashboard/tags/[tagId]/page.tsx rename to apps/web/app/dashboard/labels/[labelId]/page.tsx diff --git a/apps/web/app/dashboard/labels/page.tsx b/apps/web/app/dashboard/labels/page.tsx new file mode 100644 index 0000000..5e549a8 --- /dev/null +++ b/apps/web/app/dashboard/labels/page.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import LabelsView from "@/components/dashboard/labels/LabelsView"; + +export default async function LabelsPage() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/web/app/dashboard/tags/page.tsx b/apps/web/app/dashboard/tags/page.tsx deleted file mode 100644 index 6caea51..0000000 --- a/apps/web/app/dashboard/tags/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import AllTagsView from "@/components/dashboard/tags/AllTagsView"; -import { Separator } from "@/components/ui/separator"; -import { api } from "@/server/api/client"; - -export default async function TagsPage() { - const allTags = (await api.tags.list()).tags; - - return ( -
- All Tags - - -
- ); -} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 6e0547d..0ce5bdf 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -4,7 +4,7 @@ import { getServerAuthSession } from "@/server/auth"; export default async function Home() { const session = await getServerAuthSession(); if (session) { - redirect("/dashboard/settings"); + redirect("/dashboard/today"); } else { redirect("/signin"); } diff --git a/apps/web/components/dashboard/labels/AddLabelDialog.tsx b/apps/web/components/dashboard/labels/AddLabelDialog.tsx new file mode 100644 index 0000000..f49920a --- /dev/null +++ b/apps/web/components/dashboard/labels/AddLabelDialog.tsx @@ -0,0 +1,156 @@ +import { useEffect, useState } from "react"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { TRPCClientError } from "@trpc/client"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { HexColorPicker } from "react-colorful"; +import { zLabelSchema } from "@lifetracker/shared/types/labels"; + +type CreateLabelSchema = z.infer; + +export default function AddLabelDialog({ + children, +}: { + children?: React.ReactNode; +}) { + const apiUtils = api.useUtils(); + const [isOpen, onOpenChange] = useState(false); + const form = useForm({ + resolver: zodResolver(zLabelSchema), + defaultValues: { + code: -1, + name: "", + description: "", + color: "#000022", + }, + + }); + const { mutate, isPending } = api.labels.createLabel.useMutation({ + onSuccess: () => { + toast({ + description: "Label created successfully", + }); + onOpenChange(false); + }, + onError: (error) => { + if (error instanceof TRPCClientError) { + toast({ + variant: "destructive", + description: error.message, + }); + } else { + toast({ + variant: "destructive", + description: "Failed to create user", + }); + } + }, + }); + + useEffect(() => { + if (!isOpen) { + form.reset(); + } + }, [isOpen, form]); + + return ( + + {children} + + + New Label + +
+ mutate(data))}> +
+ +
+ +
+ ( + + Name + + + + + + )} + /> + + ( + + Description + + + + + + )} + /> + + + + + + Create + + +
+
+ +
+
+ ); +} diff --git a/apps/web/components/dashboard/tags/DeleteTagConfirmationDialog.tsx b/apps/web/components/dashboard/labels/DeleteTagConfirmationDialog.tsx similarity index 100% rename from apps/web/components/dashboard/tags/DeleteTagConfirmationDialog.tsx rename to apps/web/components/dashboard/labels/DeleteTagConfirmationDialog.tsx diff --git a/apps/web/components/dashboard/tags/EditableTagName.tsx b/apps/web/components/dashboard/labels/EditableTagName.tsx similarity index 100% rename from apps/web/components/dashboard/tags/EditableTagName.tsx rename to apps/web/components/dashboard/labels/EditableTagName.tsx diff --git a/apps/web/components/dashboard/labels/LabelsView.tsx b/apps/web/components/dashboard/labels/LabelsView.tsx new file mode 100644 index 0000000..5647b5f --- /dev/null +++ b/apps/web/components/dashboard/labels/LabelsView.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { ActionButtonWithTooltip } from "@/components/ui/action-button"; +import { ButtonWithTooltip } from "@/components/ui/button"; +import LoadingSpinner from "@/components/ui/spinner"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; +import { Check, KeyRound, Pencil, Trash, FilePlus, X } from "lucide-react"; +import { useSession } from "next-auth/react"; +import AddLabelDialog from "./AddLabelDialog"; + +export default function LabelsView() { + const { data: session } = useSession(); + const { data: labels } = api.labels.list.useQuery(); + + const LabelsTable = ({ labels }) => ( + + + Code + Name + Description + Entries With Label + Actions + + + {labels.labels.map((l) => ( + + {l.code} + {l.name} + {l.description} + + {labelStats[l.id].numEntries} + + + deleteLabel({ labelId: l.id })} + loading={false} + > + + + + + + + + + + ))} + +
+ ); + + return ( + <> +
+ All Labels + + + + + +
+ + {labels === undefined ? ( + + ) : ( + + )} + + ); +} diff --git a/apps/web/components/dashboard/tags/MergeTagModal.tsx b/apps/web/components/dashboard/labels/MergeTagModal.tsx similarity index 100% rename from apps/web/components/dashboard/tags/MergeTagModal.tsx rename to apps/web/components/dashboard/labels/MergeTagModal.tsx diff --git a/apps/web/components/dashboard/tags/TagOptions.tsx b/apps/web/components/dashboard/labels/TagOptions.tsx similarity index 100% rename from apps/web/components/dashboard/tags/TagOptions.tsx rename to apps/web/components/dashboard/labels/TagOptions.tsx diff --git a/apps/web/components/dashboard/tags/TagPill.tsx b/apps/web/components/dashboard/labels/TagPill.tsx similarity index 100% rename from apps/web/components/dashboard/tags/TagPill.tsx rename to apps/web/components/dashboard/labels/TagPill.tsx diff --git a/apps/web/components/dashboard/tags/TagSelector.tsx b/apps/web/components/dashboard/labels/TagSelector.tsx similarity index 100% rename from apps/web/components/dashboard/tags/TagSelector.tsx rename to apps/web/components/dashboard/labels/TagSelector.tsx diff --git a/apps/web/components/dashboard/sidebar/Sidebar.tsx b/apps/web/components/dashboard/sidebar/Sidebar.tsx index 451e93f..bd5bfdd 100644 --- a/apps/web/components/dashboard/sidebar/Sidebar.tsx +++ b/apps/web/components/dashboard/sidebar/Sidebar.tsx @@ -39,14 +39,9 @@ export default async function Sidebar() { }, ...searchItem, { - name: "Tags", + name: "Labels", icon: , - path: "/dashboard/tags", - }, - { - name: "Archive", - icon: , - path: "/dashboard/archive", + path: "/dashboard/labels", }, ]; diff --git a/apps/web/components/dashboard/tags/AllTagsView.tsx b/apps/web/components/dashboard/tags/AllTagsView.tsx deleted file mode 100644 index 72f4dc1..0000000 --- a/apps/web/components/dashboard/tags/AllTagsView.tsx +++ /dev/null @@ -1,210 +0,0 @@ -"use client"; - -import React from "react"; -import { ActionButton } from "@/components/ui/action-button"; -import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; -import { Button } from "@/components/ui/button"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import InfoTooltip from "@/components/ui/info-tooltip"; -import { Separator } from "@/components/ui/separator"; -import { Toggle } from "@/components/ui/toggle"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { ArrowDownAZ, Combine } from "lucide-react"; - -import type { ZGetTagResponse } from "@hoarder/shared/types/tags"; -import { useDeleteUnusedTags } from "@hoarder/shared-react/hooks/tags"; - -import DeleteTagConfirmationDialog from "./DeleteTagConfirmationDialog"; -import { TagPill } from "./TagPill"; - -function DeleteAllUnusedTags({ numUnusedTags }: { numUnusedTags: number }) { - const { mutate, isPending } = useDeleteUnusedTags({ - onSuccess: () => { - toast({ - description: `Deleted all ${numUnusedTags} unused tags`, - }); - }, - onError: () => { - toast({ - description: "Something went wrong", - variant: "destructive", - }); - }, - }); - return ( - ( - mutate()} - > - DELETE THEM ALL - - )} - > - - - ); -} - -const byUsageSorter = (a: ZGetTagResponse, b: ZGetTagResponse) => { - // Sort by name if the usage is the same to get a stable result - if (b.numBookmarks == a.numBookmarks) { - return byNameSorter(a, b); - } - return b.numBookmarks - a.numBookmarks; -}; -const byNameSorter = (a: ZGetTagResponse, b: ZGetTagResponse) => - a.name.localeCompare(b.name, undefined, { sensitivity: "base" }); - -export default function AllTagsView({ - initialData, -}: { - initialData: ZGetTagResponse[]; -}) { - interface Tag { - id: string; - name: string; - } - - const [draggingEnabled, setDraggingEnabled] = React.useState(false); - const [sortByName, setSortByName] = React.useState(false); - - const [isDialogOpen, setIsDialogOpen] = React.useState(false); - const [selectedTag, setSelectedTag] = React.useState(null); - - const handleOpenDialog = (tag: Tag) => { - setSelectedTag(tag); - setIsDialogOpen(true); - }; - - function toggleSortByName(): void { - setSortByName(!sortByName); - } - - function toggleDraggingEnabled(): void { - setDraggingEnabled(!draggingEnabled); - } - - const { data } = api.tags.list.useQuery(undefined, { - initialData: { tags: initialData }, - }); - // Sort tags by usage desc - const allTags = data.tags.sort(sortByName ? byNameSorter : byUsageSorter); - - const humanTags = allTags.filter( - (t) => (t.numBookmarksByAttachedType.human ?? 0) > 0, - ); - const aiTags = allTags.filter( - (t) => (t.numBookmarksByAttachedType.ai ?? 0) > 0, - ); - const emptyTags = allTags.filter((t) => t.numBookmarks === 0); - - const tagsToPill = (tags: typeof allTags) => { - let tagPill; - if (tags.length) { - tagPill = ( -
- {tags.map((t) => ( - - ))} -
- ); - } else { - tagPill = "No Tags"; - } - return tagPill; - }; - return ( - <> - {selectedTag && ( - { - if (!o) { - setSelectedTag(null); - } - setIsDialogOpen(o); - }} - /> - )} -
- - - Drag & Drop Merging - -

Drag and drop tags on each other to merge them

-
-
- - Sort by Name - -
- -

Your Tags

- -

Tags that were attached at least once by you

-
-
- {tagsToPill(humanTags)} - - -

AI Tags

- -

Tags that were only attached automatically (by AI)

-
-
- {tagsToPill(aiTags)} - - -

Unused Tags

- -

Tags that are not attached to any bookmarks

-
-
- -
- - - - {emptyTags.length > 0 && ( - - )} -
- {tagsToPill(emptyTags)} -
- - ); -} diff --git a/apps/web/package.json b/apps/web/package.json index 248e593..d0c632a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,11 +18,11 @@ "@auth/drizzle-adapter": "^1.4.2", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", + "@hookform/resolvers": "^3.3.4", "@lifetracker/db": "workspace:^", "@lifetracker/shared": "workspace:^0.1.0", "@lifetracker/shared-react": "workspace:^0.1.0", "@lifetracker/trpc": "workspace:^", - "@hookform/resolvers": "^3.3.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -58,6 +58,7 @@ "next-themes": "^0.3.0", "prettier": "^3.2.5", "react": "^18.2.0", + "react-colorful": "^5.6.1", "react-dom": "^18.2.0", "react-draggable": "^4.4.6", "react-dropzone": "^14.2.3", diff --git a/packages/db/migrations/0005_ambitious_famine.sql b/packages/db/migrations/0005_ambitious_famine.sql new file mode 100644 index 0000000..c868796 --- /dev/null +++ b/packages/db/migrations/0005_ambitious_famine.sql @@ -0,0 +1,11 @@ +CREATE TABLE `label` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `createdAt` integer NOT NULL, + `userId` text NOT NULL, + FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `labels_name_idx` ON `label` (`name`);--> statement-breakpoint +CREATE INDEX `labels_userId_idx` ON `label` (`userId`);--> statement-breakpoint +CREATE UNIQUE INDEX `label_userId_name_unique` ON `label` (`userId`,`name`); \ No newline at end of file diff --git a/packages/db/migrations/0006_milky_hellion.sql b/packages/db/migrations/0006_milky_hellion.sql new file mode 100644 index 0000000..23c6efd --- /dev/null +++ b/packages/db/migrations/0006_milky_hellion.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS `labels_name_idx`;--> statement-breakpoint +DROP INDEX IF EXISTS `labels_userId_idx`;--> statement-breakpoint +ALTER TABLE `label` ADD `description` text;--> statement-breakpoint +ALTER TABLE `label` ADD `color` text DEFAULT '#000000'; \ No newline at end of file diff --git a/packages/db/migrations/meta/0005_snapshot.json b/packages/db/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..3ccb61e --- /dev/null +++ b/packages/db/migrations/meta/0005_snapshot.json @@ -0,0 +1,497 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "06e873b5-ab39-469c-9437-bb1cf4bc60e8", + "prevId": "bcdf4680-869f-4b4a-9690-1bd45a196387", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "day": { + "name": "day", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "day_date_unique": { + "name": "day_date_unique", + "columns": [ + "date" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "label": { + "name": "label", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "labels_name_idx": { + "name": "labels_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "labels_userId_idx": { + "name": "labels_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "label_userId_name_unique": { + "name": "label_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "label_userId_user_id_fk": { + "name": "label_userId_user_id_fk", + "tableFrom": "label", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/0006_snapshot.json b/packages/db/migrations/meta/0006_snapshot.json new file mode 100644 index 0000000..849d489 --- /dev/null +++ b/packages/db/migrations/meta/0006_snapshot.json @@ -0,0 +1,498 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "22c67302-ebcd-4ad9-8393-feea41c17455", + "prevId": "06e873b5-ab39-469c-9437-bb1cf4bc60e8", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "config": { + "name": "config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "day": { + "name": "day", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "day_date_unique": { + "name": "day_date_unique", + "columns": [ + "date" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "label": { + "name": "label", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'#000000'" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "label_userId_name_unique": { + "name": "label_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "label_userId_user_id_fk": { + "name": "label_userId_user_id_fk", + "tableFrom": "label", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 21f43b3..a2d2e96 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -36,6 +36,20 @@ "when": 1731637559763, "tag": "0004_sad_sally_floyd", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1731648881226, + "tag": "0005_ambitious_famine", + "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1731649046343, + "tag": "0006_milky_hellion", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 2571cf5..14bf568 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -105,6 +105,26 @@ export const days = sqliteTable("day", { comment: text("comment").notNull(), }); +export const labels = sqliteTable( + "label", + { + id: text("id") + .notNull() + .primaryKey() + .$defaultFn(() => createId()), + createdAt: createdAtField(), + name: text("name").notNull(), + description: text("description"), + color: text("color").default("#000000"), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + }, + (lb) => ({ + uniq: unique().on(lb.userId, lb.name) + }), +); + export const config = sqliteTable("config", { key: text("key").notNull().primaryKey(), value: text("value").notNull(), @@ -118,3 +138,17 @@ export const apiKeyRelations = relations(apiKeys, ({ one }) => ({ references: [users.id], }), })); +export const userRelations = relations(users, ({ many }) => ({ + labels: many(labels), +})); + + +export const labelsRelations = relations( + labels, + ({ many, one }) => ({ + user: one(users, { + fields: [labels.userId], + references: [users.id], + }), + }), +); \ No newline at end of file diff --git a/packages/shared/types/labels.ts b/packages/shared/types/labels.ts new file mode 100644 index 0000000..4000007 --- /dev/null +++ b/packages/shared/types/labels.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const zLabelSchema = z.object({ + id: z.string(), + code: z.coerce.number(), + name: z.string(), + color: z.string().default("#000000"), + description: z.string().optional(), +}); +export type ZLabels = z.infer; + +export const zGetLabelResponseSchema = z.object({ + id: z.string(), + code: z.number(), + name: z.string(), + numEntries: z.number(), +}); +export type ZGetLabelResponse = z.infer; + +export const zUpdateLabelRequestSchema = z.object({ + labelId: z.string(), + code: z.number(), + name: z.string().optional(), +}); \ No newline at end of file diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts index bafafa7..0cdd3f3 100644 --- a/packages/trpc/routers/_app.ts +++ b/packages/trpc/routers/_app.ts @@ -3,11 +3,13 @@ import { router } from "../index"; import { usersAppRouter } from "./users"; import { apiKeysAppRouter } from "./apiKeys"; import { adminAppRouter } from "./admin"; +import { labelsAppRouter } from "./labels"; export const appRouter = router({ users: usersAppRouter, apiKeys: apiKeysAppRouter, admin: adminAppRouter, + labels: labelsAppRouter, }); // export type definition of API export type AppRouter = typeof appRouter; \ No newline at end of file diff --git a/packages/trpc/routers/labels.ts b/packages/trpc/routers/labels.ts new file mode 100644 index 0000000..d116869 --- /dev/null +++ b/packages/trpc/routers/labels.ts @@ -0,0 +1,146 @@ +import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; +import { and, desc, eq, inArray, notExists } from "drizzle-orm"; +import { z } from "zod"; + +import { SqliteError } from "@lifetracker/db"; +import { labels } from "@lifetracker/db/schema"; +import { + zLabelSchema, + zGetLabelResponseSchema, +} from "@lifetracker/shared/types/labels"; +import type { Context } from "../index"; +import { authedProcedure, router } from "../index"; + + +function conditionFromInput(input: { labelId: string }, userId: string) { + return and(eq(labels.id, input.labelId), eq(labels.userId, userId)); +} + +async function createLabel( + input: z.infer, + ctx: Context, +) { + return ctx.db.transaction(async (trx) => { + + try { + const result = await trx + .insert(labels) + .values({ + name: input.name, + code: input.code, + description: input.description, + color: input.color, + userId: ctx.user!.id, + }) + .returning({ + id: labels.id, + name: labels.name, + code: labels.code, + description: labels.description, + color: labels.color, + }); + return result[0]; + } catch (e) { + if (e instanceof SqliteError) { + if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Line 48 trpc routers labels.ts", + }); + } + } + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Something went wrong", + }); + } + }); +} + +export const labelsAppRouter = router({ + list: authedProcedure + .output( + z.object({ + labels: z.array(zGetLabelResponseSchema), + }), + ) + .query(async ({ ctx }) => { + const res = await ctx.db + .select({ + id: labels.id, + name: labels.name, + color: labels.color, + description: labels.description, + }) + .from(labels) + .where(eq(labels.userId, ctx.user.id)); + + return { + labels: res.map((r) => ({ + id: r.id, + name: r.name, + color: r.color, + description: r.description, + numEntries: 420, + })), + }; + }), + get: authedProcedure + .input( + z.object({ + labelId: z.string(), + }), + ) + .output(zGetLabelResponseSchema) + .query(async ({ input, ctx }) => { + const res = await ctx.db + .select({ + id: labels.id, + name: labels.name + }) + .from(labels) + .where( + and( + conditionFromInput(input, ctx.user.id), + eq(labels.userId, ctx.user.id), + ), + ); + + if (res.length == 0) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + const numEntriesWithLabel = res.reduce< + Record + >( + (acc, curr) => { + if (curr.labeledBy) { + acc[curr.labeledBy]++; + } + return acc; + }, + { ai: 0, human: 0 }, + ); + + return { + id: res[0].id, + name: res[0].name, + numEntries: 420 + }; + }), + createLabel: authedProcedure + .input(zLabelSchema) + .output( + z.object({ + id: z.string(), + code: z.number(), + name: z.string(), + color: z.string().default("#000000"), + description: z.string().optional(), + }), + ) + .mutation(async ({ input, ctx }) => { + console.log("Just started creating a label"); + return createLabel(input, ctx); + }), +}); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0151e3a..d484677 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,6 +256,9 @@ importers: react: specifier: ^18.2.0 version: 18.3.1 + react-colorful: + specifier: ^5.6.1 + version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) @@ -9707,6 +9710,12 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-colorful@5.6.1: + resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-dev-utils@12.0.1: resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} engines: {node: '>=14'} @@ -19684,7 +19693,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-react: 7.33.2(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) @@ -19738,7 +19747,7 @@ snapshots: enhanced-resolve: 5.15.0 eslint: 8.57.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.1 get-tsconfig: 4.7.2 is-core-module: 2.13.1 @@ -19777,7 +19786,7 @@ snapshots: eslint: 8.57.0 ignore: 5.3.1 - eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 @@ -24083,6 +24092,11 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-colorful@5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-dev-utils@12.0.1(eslint@8.57.0)(typescript@5.3.3)(webpack@5.95.0): dependencies: '@babel/code-frame': 7.22.13