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 (
-
- );
-}
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 (
+
+ );
+}
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 (
+ <>
+
+
+ {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