Now able to create categories!

This commit is contained in:
Ryan Pandya 2024-11-24 02:33:20 -08:00
parent 47e8371fb3
commit 78144d0083
17 changed files with 364 additions and 103 deletions

View File

@ -0,0 +1,106 @@
import { getGlobalOptions } from "@/lib/globals";
import {
printError,
printErrorMessageWithReason,
printObject,
printSuccess,
} from "@/lib/output";
import { getAPIClient } from "@/lib/trpc";
import { Command } from "@commander-js/extra-typings";
import { getBorderCharacters, table } from "table";
export const categoriesCmd = new Command()
.name("categories")
.description("Manipulate categories");
categoriesCmd
.command("list")
.description("lists all defined categories")
.action(async () => {
const api = getAPIClient();
try {
const categories = (await api.categories.list.query()).categories;
// colors.sort((a, b) => b.numCategories - a.numCategories);
if (getGlobalOptions().json) {
printObject(categories);
} else {
const data: string[][] = [["Code", "Name", "Description", "Color"]];
categories.forEach((c) => {
data.push([c.code.toString(), c.name, c.description ?? "none", c.color.name]);
});
console.log(
data.length <= 1 ?
"No categories found. Add one with `lifetracker categories create <code> <name> <description> <color>`" :
table(data, {
// border: getBorderCharacters("ramac"),
// singleLine: true,
drawVerticalLine: (lineIndex, columnCount) => {
return lineIndex === 0 || lineIndex === columnCount;
},
drawHorizontalLine: (lineIndex, rowCount) => {
return lineIndex < 2 || lineIndex === rowCount;
},
}),
);
}
} catch (error) {
printErrorMessageWithReason("Failed to list all categories", error as object);
}
});
categoriesCmd
.command("create")
.description("create a category")
.argument("<code>", "the code of the category")
.argument("<name>", "the name of the category")
.argument("<description>", "the description of the category")
.argument("[color]", "the color of the category")
.option('-p, --parent <CODE>', "specify a parent category by code")
.action(async (codeStr: string, name: string, description: string, colorName?: string, flags?) => {
const api = getAPIClient();
const color = flags?.parent === undefined ?
(await api.colors.get.query({ colorName: colorName! }))
:
(await api.categories.get.query({ categoryCode: parseInt(flags.parent) })).color
try {
await api.categories.create
.mutate({
code: parseFloat(codeStr),
name: name,
description: description,
color: color,
})
.then(printSuccess(`Successfully created the category "${name}"`))
.catch(printError(`Failed to create the category "${name}"`));
api.categories.list
}
catch (e) {
console.log(e);
}
});
categoriesCmd
.command("delete")
.description("delete a category")
.argument("<code>", "the code of the category")
.action(async (codeStr: string) => {
const api = getAPIClient();
try {
await api.categories.delete
.mutate({
categoryCode: parseInt(codeStr),
})
.then(printSuccess(`Successfully deleted category "${codeStr}"`))
.catch(printError(`Failed to delete category "${codeStr}"`));
}
catch (e) {
console.log(e);
}
});

View File

@ -25,17 +25,23 @@ colorsCmd
if (getGlobalOptions().json) { if (getGlobalOptions().json) {
printObject(colors); printObject(colors);
} else { } else {
const data: string[][] = [["Id", "Name", "Hexcode", "user"]]; const data: string[][] = [["Name", "Hexcode", "Inverse"]];
colors.forEach((color) => { colors.forEach((color) => {
data.push([color.id, color.name, color.hexcode, color.userId]); data.push([color.name, color.hexcode, color.inverse]);
}); });
console.log( console.log(
data.length <= 1 ? data.length <= 1 ?
"No colors found. Add one with `lifetracker colors create <name> <hexcode>`" : "No colors found. Add one with `lifetracker colors create <name> <hexcode>`" :
table(data, { table(data, {
border: getBorderCharacters("ramac"), // border: getBorderCharacters("ramac"),
singleLine: true, // singleLine: true,
drawVerticalLine: (lineIndex, columnCount) => {
return lineIndex === 0 || lineIndex === columnCount;
},
drawHorizontalLine: (lineIndex, rowCount) => {
return lineIndex < 2 || lineIndex === rowCount;
},
}), }),
); );
} }

View File

@ -3,6 +3,7 @@ import { Command, Option } from "@commander-js/extra-typings";
import { whoamiCmd } from "@/commands/whoami"; import { whoamiCmd } from "@/commands/whoami";
import { colorsCmd } from "@/commands/colors"; import { colorsCmd } from "@/commands/colors";
import { categoriesCmd } from "@/commands/categories";
import { config } from "dotenv"; import { config } from "dotenv";
@ -34,11 +35,9 @@ const program = new Command()
: "0.0.0", : "0.0.0",
); );
program.addCommand(whoamiCmd, { program.addCommand(whoamiCmd);
isDefault: true
});
program.addCommand(colorsCmd); program.addCommand(colorsCmd);
program.addCommand(categoriesCmd);
setGlobalOptions(program.opts()); setGlobalOptions(program.opts());

View File

@ -52,7 +52,7 @@ export default function CategoriesView() {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{categories.categories.map((c) => ( {categories.categories.map((c) => (
<TableRow key={c.id} style={{ backgroundColor: c.color }}> <TableRow key={c.id} style={{ backgroundColor: c.color.hexcode }}>
<TableCell className="py-1">{c.code}</TableCell> <TableCell className="py-1">{c.code}</TableCell>
<TableCell className="py-1">{c.name}</TableCell> <TableCell className="py-1">{c.name}</TableCell>
<TableCell className="py-1">{c.description}</TableCell> <TableCell className="py-1">{c.description}</TableCell>

View File

@ -33,9 +33,9 @@ import { TRPCClientError } from "@trpc/client";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { zLabelSchema } from "@lifetracker/shared/types/categories"; import { zCategorySchema } from "@lifetracker/shared/types/categories";
type CreateLabelSchema = z.infer<typeof zCategorySchema>; type CreateCategorySchema = z.infer<typeof zCategorySchema>;
export default function EditCategoryDialog({ export default function EditCategoryDialog({
children, children,

View File

@ -19,26 +19,26 @@ import AddColor from "./AddColor";
// import EditCategoryDialog from "./EditCategoryDialog"; // import EditCategoryDialog from "./EditCategoryDialog";
import Link from "next/link"; import Link from "next/link";
export default function ColorssView() { export default function ColorsView() {
const { data: session } = useSession(); const { data: session } = useSession();
const { data: colors } = api.colors.list.useQuery(); const { data: colors } = api.colors.list.useQuery();
// const invalidateColorList = api.useUtils().colors.list.invalidate; const invalidateColorList = api.useUtils().colors.list.invalidate;
// const { mutate: deleteColor, isPending: isDeletionPending } = const { mutate: deleteColor, isPending: isDeletionPending } =
// api.colors.delete.useMutation({ api.colors.delete.useMutation({
// onSuccess: () => { onSuccess: () => {
// toast({ toast({
// description: "Color deleted", description: "Color deleted",
// }); });
// invalidateCategoryList(); invalidateColorList();
// }, },
// onError: (e) => { onError: (e) => {
// toast({ toast({
// variant: "destructive", variant: "destructive",
// description: `Something went wrong: ${e.message}`, description: `Something went wrong: ${e.message}`,
// }); });
// }, },
// }); });
const ColorsTable = ({ colors }) => ( const ColorsTable = ({ colors }) => (
<Table> <Table>
@ -52,13 +52,13 @@ export default function ColorssView() {
<TableRow key={c.id} style={{ backgroundColor: c.hexcode, color: c.inverse }}> <TableRow key={c.id} style={{ backgroundColor: c.hexcode, color: c.inverse }}>
<TableCell className="py-1">{c.name}</TableCell> <TableCell className="py-1">{c.name}</TableCell>
<TableCell className="py-1"> <TableCell className="py-1">
numEntries {c.numCategories}
</TableCell> </TableCell>
<TableCell className="flex gap-1 py-1"> <TableCell className="flex gap-1 py-1">
<ActionButtonWithTooltip <ActionButtonWithTooltip
tooltip="Delete category" tooltip="Delete category"
variant="outline" variant="outline"
onClick={() => deleteColor({ colorId: c.id })} onClick={() => deleteColor({ colorName: c.name })}
loading={false} loading={false}
> >
<Trash size={16} color="red" /> <Trash size={16} color="red" />

View File

@ -28,7 +28,7 @@ CREATE TABLE `category` (
`id` text PRIMARY KEY NOT NULL, `id` text PRIMARY KEY NOT NULL,
`createdAt` integer NOT NULL, `createdAt` integer NOT NULL,
`name` text NOT NULL, `name` text NOT NULL,
`code` integer NOT NULL, `code` real NOT NULL,
`description` text, `description` text,
`colorId` text NOT NULL, `colorId` text NOT NULL,
`parentId` text, `parentId` text,
@ -86,7 +86,7 @@ CREATE TABLE `verificationToken` (
--> statement-breakpoint --> statement-breakpoint
CREATE UNIQUE INDEX `apiKey_keyId_unique` ON `apiKey` (`keyId`);--> statement-breakpoint CREATE UNIQUE INDEX `apiKey_keyId_unique` ON `apiKey` (`keyId`);--> statement-breakpoint
CREATE UNIQUE INDEX `apiKey_name_userId_unique` ON `apiKey` (`name`,`userId`);--> statement-breakpoint CREATE UNIQUE INDEX `apiKey_name_userId_unique` ON `apiKey` (`name`,`userId`);--> statement-breakpoint
CREATE UNIQUE INDEX `category_userId_name_unique` ON `category` (`userId`,`name`);--> statement-breakpoint CREATE UNIQUE INDEX `category_userId_code_unique` ON `category` (`userId`,`code`);--> statement-breakpoint
CREATE UNIQUE INDEX `color_userId_name_unique` ON `color` (`userId`,`name`);--> statement-breakpoint CREATE UNIQUE INDEX `color_userId_name_unique` ON `color` (`userId`,`name`);--> statement-breakpoint
CREATE UNIQUE INDEX `day_date_unique` ON `day` (`date`);--> statement-breakpoint CREATE UNIQUE INDEX `day_date_unique` ON `day` (`date`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);

View File

@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "d35127c1-6892-410e-b933-d7ec7aabe6f5", "id": "7f1d6157-2405-4b96-be6a-a4bfb35f9aaf",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"account": { "account": {
@ -219,7 +219,7 @@
}, },
"code": { "code": {
"name": "code", "name": "code",
"type": "integer", "type": "real",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
@ -254,11 +254,11 @@
} }
}, },
"indexes": { "indexes": {
"category_userId_name_unique": { "category_userId_code_unique": {
"name": "category_userId_name_unique", "name": "category_userId_code_unique",
"columns": [ "columns": [
"userId", "userId",
"name" "code"
], ],
"isUnique": true "isUnique": true
} }

View File

@ -5,8 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1732260188078, "when": 1732441653703,
"tag": "0000_gigantic_doctor_strange", "tag": "0000_awesome_stepford_cuckoos",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@ -1,27 +0,0 @@
import { db } from "./drizzle";
import { apiKeys, users } from "./schema";
import { generateApiKey } from "../trpc/auth";
import { and, count, eq } from "drizzle-orm";
db.transaction(async (trx) => {
const ryan = await trx
.insert(users)
.values({
name: "Ryan Pandya",
email: "ryan@ryanpandya.com",
password: "$2a$10$ngv9752uxDT11hSPfdZmAe2D8VXLB9mcXkN7TRPI5GZQCuriIu1gC",
role: "admin",
})
.returning({
id: users.id,
name: users.name,
email: users.email,
role: users.role,
});
db.query.users.findFirst({
where: eq(users.email, "ryan@ryanpandya.com"),
}).then((user) => {
generateApiKey("CLI App", ryan.id);
});
});

View File

@ -5,6 +5,7 @@ import {
AnySQLiteColumn, AnySQLiteColumn,
index, index,
integer, integer,
real,
primaryKey, primaryKey,
sqliteTable, sqliteTable,
text, text,
@ -28,6 +29,10 @@ export function calcInverseColor(hexcode: string): string {
return luminance > 186 ? "#000000" : "#ffffff"; return luminance > 186 ? "#000000" : "#ffffff";
} }
// export function calcCategoryParent(parentId: string | null){
// return parentId ??
// }
export const config = sqliteTable("config", { export const config = sqliteTable("config", {
@ -155,7 +160,7 @@ export const categories = sqliteTable(
.$defaultFn(() => createId()), .$defaultFn(() => createId()),
createdAt: createdAtField(), createdAt: createdAtField(),
name: text("name").notNull(), name: text("name").notNull(),
code: integer("code").notNull(), code: real("code").notNull(),
description: text("description"), description: text("description"),
colorId: text("colorId") colorId: text("colorId")
.notNull() .notNull()
@ -165,8 +170,8 @@ export const categories = sqliteTable(
.notNull() .notNull()
.references(() => users.id, { onDelete: "cascade" }), .references(() => users.id, { onDelete: "cascade" }),
}, },
(lb) => ({ (c) => ({
uniq: unique().on(lb.userId, lb.name) uniq: unique().on(c.userId, c.code)
}), }),
); );

View File

@ -1,14 +1,26 @@
import { z } from "zod"; import { z } from "zod";
import { zColorSchema } from "./colors";
export const zCategorySchema = z.object({ export const zCategorySchema = z.object({
id: z.string(),
code: z.coerce.number(), code: z.coerce.number(),
name: z.string(), name: z.string(),
colorId: z.string(),
description: z.string().optional(), description: z.string().optional(),
color: zColorSchema,
parentId: z.string().optional(),
}); });
export type ZCategories = z.infer<typeof zCategorySchema>; export type ZCategories = z.infer<typeof zCategorySchema>;
export const zCreateCategorySchema = z.object({
code: z.coerce.number(),
name: z.string(),
description: z.string().optional(),
color: z.string(),
parentId: z.string().optional(),
});
export type ZCreateCategories = z.infer<typeof zCreateCategorySchema>;
export const zGetCategoryResponseSchema = z.object({ export const zGetCategoryResponseSchema = z.object({
id: z.string(), id: z.string(),
code: z.number(), code: z.number(),

View File

@ -3,5 +3,7 @@ import { z } from "zod";
export const zColorSchema = z.object({ export const zColorSchema = z.object({
name: z.string(), name: z.string(),
hexcode: z.string(), hexcode: z.string(),
inverse: z.string().optional(),
id: z.string().optional(),
}); });
export type ZColor = z.infer<typeof zColorSchema>; export type ZColor = z.infer<typeof zColorSchema>;

View File

@ -3,18 +3,22 @@ import { and, desc, eq, inArray, notExists } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { SqliteError } from "@lifetracker/db"; import { SqliteError } from "@lifetracker/db";
import { categories } from "@lifetracker/db/schema"; import { categories, colors } from "@lifetracker/db/schema";
import { import {
ZCategories,
zCategorySchema, zCategorySchema,
ZCreateCategories,
zCreateCategorySchema,
zGetCategoryResponseSchema, zGetCategoryResponseSchema,
zUpdateCategoryRequestSchema zUpdateCategoryRequestSchema
} from "@lifetracker/shared/types/categories"; } from "@lifetracker/shared/types/categories";
import type { Context } from "../index"; import type { Context } from "../index";
import { authedProcedure, router } from "../index"; import { authedProcedure, router } from "../index";
import { zColorSchema } from "@lifetracker/shared/types/colors";
function conditionFromInput(input: { categoryId: string }, userId: string) { function conditionFromInput(input: { categoryCode: string }, userId: string) {
return and(eq(categories.id, input.categoryId), eq(categories.userId, userId)); return and(eq(categories.code, input.categoryCode), eq(categories.userId, userId));
} }
async function createCategory( async function createCategory(
@ -22,10 +26,8 @@ async function createCategory(
ctx: Context, ctx: Context,
) { ) {
console.log("Creating a category");
return ctx.db.transaction(async (trx) => { return ctx.db.transaction(async (trx) => {
console.log("Creating a category", input);
try { try {
const result = await trx const result = await trx
.insert(categories) .insert(categories)
@ -33,23 +35,31 @@ async function createCategory(
name: input.name, name: input.name,
code: input.code, code: input.code,
description: input.description, description: input.description,
color: input.color, colorId: input.color.id,
userId: ctx.user!.id, userId: ctx.user!.id,
parentId: input.parentId ?? undefined
}) })
.returning({ .returning({
id: categories.id, id: categories.id,
name: categories.name, name: categories.name,
code: categories.code, code: categories.code,
description: categories.description, description: categories.description,
color: categories.color, colorId: categories.colorId
}); });
return result[0]; const { colorId, ...newCategory } = result[0];
return {
color: input.color,
...newCategory
};
} catch (e) { } catch (e) {
console.log(e);
if (e instanceof SqliteError) { if (e instanceof SqliteError) {
if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Line 48 trpc routers categories.ts", message: "There's already a category with this code",
}); });
} }
} }
@ -93,7 +103,10 @@ export const categoriesAppRouter = router({
categories: z.array( categories: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
code: z.number(),
name: z.string(), name: z.string(),
description: z.string().optional(),
color: zColorSchema,
}), }),
), ),
}), }),
@ -102,34 +115,43 @@ export const categoriesAppRouter = router({
const dbCategories = await ctx.db const dbCategories = await ctx.db
.select({ .select({
id: categories.id, id: categories.id,
code: categories.code,
name: categories.name, name: categories.name,
description: categories.description,
color: {
name: colors.name,
hexcode: colors.hexcode,
inverse: colors.inverse
}
}) })
.from(categories); .from(categories)
.leftJoin(colors, eq(categories.colorId, colors.id))
// console.log("Listing cats"); ;
// console.log(dbCategories);
// console.log(dbCategories.map(({ ...category }) => ({
// ...category
// })));
return { return {
categories: dbCategories.map(({ ...category }) => ({ categories: dbCategories.map(({ color, ...category }) => ({
...category ...category,
color,
})), })),
}; };
}), }),
get: authedProcedure get: authedProcedure
.input( .input(
z.object({ z.object({
categoryId: z.string(), categoryCode: z.number(),
}), }),
) )
.output(zGetCategoryResponseSchema) .output(zCategorySchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const res = await ctx.db const res = await ctx.db
.select({ .select({
id: categories.id, id: categories.id,
name: categories.name code: categories.code,
name: categories.name,
description: categories.description,
colorId: categories.colorId,
parentId: categories.parentId,
userId: categories.userId,
}) })
.from(categories) .from(categories)
.where( .where(
@ -155,11 +177,28 @@ export const categoriesAppRouter = router({
{ ai: 0, human: 0 }, { ai: 0, human: 0 },
); );
return { const [color] = await ctx.db.select().from(colors).where(
id: res[0].id, and(
name: res[0].name, and(eq(colors.id, res[0].colorId), eq(colors.userId, res[0].userId)),
numEntries: 420 eq(res[0].userId, ctx.user.id),
)
);
const categoryId: string = res[0].id;
const { id, ...categoryProps } = res[0];
const category: ZCategories = {
color: color,
...categoryProps
}; };
if (category.parentId == null) {
category.parentId = categoryId;
}
console.log(category);
return category;
}), }),
create: authedProcedure create: authedProcedure
.input(zCategorySchema) .input(zCategorySchema)
@ -168,12 +207,11 @@ export const categoriesAppRouter = router({
id: z.string(), id: z.string(),
code: z.number(), code: z.number(),
name: z.string(), name: z.string(),
color: z.string().default("#000000"), color: zColorSchema,
description: z.string().optional(), description: z.string().optional(),
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
console.log("Just started creating a category");
return createCategory(input, ctx); return createCategory(input, ctx);
}), }),
update: authedProcedure update: authedProcedure
@ -240,7 +278,7 @@ export const categoriesAppRouter = router({
delete: authedProcedure delete: authedProcedure
.input( .input(
z.object({ z.object({
categoryId: z.string(), categoryCode: z.number(),
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {

View File

@ -12,7 +12,7 @@ import { authedProcedure, router } from "../index";
import { titleCase } from "title-case"; import { titleCase } from "title-case";
function conditionFromInput(input: { colorName: string }, userId: string) { function conditionFromInput(input: { colorName: string }, userId: string) {
return and(eq(titleCase(colors.name), titleCase(input.colorName)), eq(colors.userId, userId)); return and(eq(colors.name, titleCase(input.colorName)), eq(colors.userId, userId));
} }
async function createColor( async function createColor(
@ -62,7 +62,8 @@ export const colorsAppRouter = router({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
hexcode: z.string(), hexcode: z.string(),
userId: z.string(), inverse: z.string(),
numCategories: z.number(),
}), }),
), ),
})) }))
@ -72,13 +73,14 @@ export const colorsAppRouter = router({
id: colors.id, id: colors.id,
name: colors.name, name: colors.name,
hexcode: colors.hexcode, hexcode: colors.hexcode,
userId: colors.userId, inverse: colors.inverse,
}) })
.from(colors); .from(colors);
return { return {
colors: dbColors.map(({ ...color }) => ({ colors: dbColors.map(({ ...color }) => ({
...color, ...color,
numCategories: 0
})), })),
}; };
}), }),
@ -108,4 +110,32 @@ export const colorsAppRouter = router({
throw new TRPCError({ code: "NOT_FOUND" }); throw new TRPCError({ code: "NOT_FOUND" });
} }
}), }),
get: authedProcedure
.input(
z.object({
colorName: z.string(),
}),
)
.output(
z.object({
id: z.string(),
name: z.string(),
hexcode: z.string(),
}))
.query(async ({ input, ctx }) => {
const res = await ctx.db
.select({
id: colors.id,
name: colors.name,
hexcode: colors.hexcode,
})
.from(colors)
.where(conditionFromInput(input, ctx.user.id));
if (res.length == 0) {
throw new TRPCError({ code: "NOT_FOUND" });
}
return res[0];
}),
}); });

View File

@ -0,0 +1,79 @@
pnpm --filter=@lifetracker/cli run run categories create 0 "Sleep" "Time spent sleeping." black
pnpm --filter=@lifetracker/cli run run categories create --parent 0 0.4 "Travel sleep" "Typically shitty half-sleep on a plane or in a car."
pnpm --filter=@lifetracker/cli run run categories create --parent 0 0.5 "Nap" "Sleep other than at night."
pnpm --filter=@lifetracker/cli run run categories create --parent 0 0.9 "Sick" "Resting while sick or injured."
pnpm --filter=@lifetracker/cli run run categories create 1 "Family" "Time spent with family." crimson
pnpm --filter=@lifetracker/cli run run categories create --parent 1 13 "Wedding, etc." "Wedding and related stuff."
pnpm --filter=@lifetracker/cli run run categories create --parent 1 15 "Neighborhood / Community" "Local family, if you will."
pnpm --filter=@lifetracker/cli run run categories create --parent 1 16 "Watching something" "TV, movies, or the like, watched with family."
pnpm --filter=@lifetracker/cli run run categories create 2 "Friends" "Time spent with friends." teal
pnpm --filter=@lifetracker/cli run run categories create --parent 2 22 "Party" "Party with friends."
pnpm --filter=@lifetracker/cli run run categories create --parent 2 26 "Active / Sports" "Playing active games or sports with friends."
pnpm --filter=@lifetracker/cli run run categories create --parent 2 28 "Meal" "Preparing or eating a meal with friends."
pnpm --filter=@lifetracker/cli run run categories create --parent 2 29 "Drugs" "Drugs with friends."
pnpm --filter=@lifetracker/cli run run categories create 3 "Jen" "Time spent on dates or primarily with/for Jen." pink
pnpm --filter=@lifetracker/cli run run categories create --parent 3 31 "Jen's family" "Time spent with Jen's family."
pnpm --filter=@lifetracker/cli run run categories create --parent 3 32 "Jen's friends" "Time with primarily Jen's friends."
pnpm --filter=@lifetracker/cli run run categories create --parent 3 33 "Sex" "Sex or sex-related activities."
pnpm --filter=@lifetracker/cli run run categories create --parent 3 34 "Exploring" "Exploring and adventuring, with a more urban (non-nature) focus."
pnpm --filter=@lifetracker/cli run run categories create --parent 3 35 "Errands & Logistics" "When shit needs to get done, for me and Jen."
pnpm --filter=@lifetracker/cli run run categories create --parent 3 36 "Watching something" "Watching a movie, TV show, or Internet stuff with Jen."
pnpm --filter=@lifetracker/cli run run categories create --parent 3 37 "Hiking / Adventuring" "Hiking, camping, and backcountry adventures with an active focus."
pnpm --filter=@lifetracker/cli run run categories create --parent 3 38 "Meal with Jen" "Cooking, eating out, or just having a meal with my love."
pnpm --filter=@lifetracker/cli run run categories create --parent 3 39 "Relaxing / Napping" "Rest and cuddles, other than primarily for sleep or sex."
pnpm --filter=@lifetracker/cli run run categories create 4 "Flying" "Anything related to private flying." blue
pnpm --filter=@lifetracker/cli run run categories create --parent 4 41 "Airports, etc." "Dealing with FBOs, getting in or out of the plane, etc."
pnpm --filter=@lifetracker/cli run run categories create --parent 4 42 "Flying with friends" "Tour or flight for fun with friends."
pnpm --filter=@lifetracker/cli run run categories create --parent 4 43 "Flying with Jen" "A flying trip primarily for me and Jen."
pnpm --filter=@lifetracker/cli run run categories create --parent 4 44 "Flight planning" "Checking weather, planning, filing flight plan, etc."
pnpm --filter=@lifetracker/cli run run categories create --parent 4 45 "Flying for work" "A primarily business-oriented flying trip."
pnpm --filter=@lifetracker/cli run run categories create --parent 4 46 "Maintenance" "A primarily maintenance-related flying trip."
pnpm --filter=@lifetracker/cli run run categories create --parent 4 47 "Training" "A primarily training-related flying trip."
pnpm --filter=@lifetracker/cli run run categories create --parent 4 49 "Other Flying Related" "e.g., Phone calls, research, buying/selling"
pnpm --filter=@lifetracker/cli run run categories create 5 "Work" "Time spent working for money." green
pnpm --filter=@lifetracker/cli run run categories create --parent 5 51 "Investors" "Pitching, diligence, calls, or somehow convincing investors to pony up."
pnpm --filter=@lifetracker/cli run run categories create --parent 5 52 "Cofounders" "Meeting with one or more of Perumal, Bonney, TM."
pnpm --filter=@lifetracker/cli run run categories create --parent 5 53 "[REMAP ME] Wedding prep" "This was set up because I was working so hard on the wedding in 2023 I thought I was going to die, but it really doesn't have much use being in the 5s."
pnpm --filter=@lifetracker/cli run run categories create --parent 5 55 "Board" "Dealing with Board members or Board matters."
pnpm --filter=@lifetracker/cli run run categories create --parent 5 56 "Emails" "Getting the inbox under control."
pnpm --filter=@lifetracker/cli run run categories create --parent 5 57 "Work meditation" "Thinking, ruminating, or journaling about work."
pnpm --filter=@lifetracker/cli run run categories create --parent 5 58 "Socializing" "Out building work-related relationships."
pnpm --filter=@lifetracker/cli run run categories create 6 "Productive" "Time spent active (working out), fulfilling obligations, running errands, doing work without pay, or otherwise in a useful and productive way." orange
pnpm --filter=@lifetracker/cli run run categories create --parent 6 61 "Cleaning" "Tidying, organizing, or cleaning a space."
pnpm --filter=@lifetracker/cli run run categories create --parent 6 62 "Relationship work" "Having important conversations or working on the big stuff."
pnpm --filter=@lifetracker/cli run run categories create --parent 6 63 "Home maintenance" "Productivity related to the home or household."
pnpm --filter=@lifetracker/cli run run categories create --parent 6 65 "Personal finances / Life admin" "Chores related to life administration and personal finances."
pnpm --filter=@lifetracker/cli run run categories create --parent 6 66 "Outdoor exercise" "Walking, running, biking, or hiking outdoors."
pnpm --filter=@lifetracker/cli run run categories create --parent 6 67 "Gym" "Working on my fitness in a gym."
pnpm --filter=@lifetracker/cli run run categories create --parent 6 68 "Meditation / Therapy" "Working on myself."
pnpm --filter=@lifetracker/cli run run categories create --parent 6 69 "Bureaucracy / Bullshit" "Working on a needed but Kafkaesque nightmare chore."
pnpm --filter=@lifetracker/cli run run categories create 7 "Hobbies & Skills" "Time used for personal development, improving skills, or what I consider to be productive hobbies." yellow
pnpm --filter=@lifetracker/cli run run categories create --parent 7 71 "Language learning" "Learning, studying, reading, or practicing a foreign language."
pnpm --filter=@lifetracker/cli run run categories create --parent 7 72 "Cooking" "Preparing food."
pnpm --filter=@lifetracker/cli run run categories create --parent 7 73 "Home improvement" "Fun or useful stuff for the house, including gardening, automation, or other projecs that are basically hobbies."
pnpm --filter=@lifetracker/cli run run categories create --parent 7 75 "Reading" "Reading an article, book, or something else edifying."
pnpm --filter=@lifetracker/cli run run categories create --parent 7 76 "Organizing / Workflow" "Working on a hobby or skill, but doing the boring / annoying bits."
pnpm --filter=@lifetracker/cli run run categories create --parent 7 78 "Programming / Computers" "Nerding out and building something with computers."
pnpm --filter=@lifetracker/cli run run categories create --parent 7 79 "Shopping" "Shopping that isn't a waste of time."
pnpm --filter=@lifetracker/cli run run categories create 8 "Relaxation & Leisure" "Time spent gaming, relaxing, consuming entertainment, or participating in passive activities." purple
pnpm --filter=@lifetracker/cli run run categories create --parent 8 81 "Watching something" "Watching something produced for the screen (TV / Movies / Tiktok / YouTube)"
pnpm --filter=@lifetracker/cli run run categories create --parent 8 82 "Hot tub / Sauna / Pool" "Sweating or swimming in a relaxing way, other than to get clean."
pnpm --filter=@lifetracker/cli run run categories create --parent 8 83 "Masturbation" "Self sex."
pnpm --filter=@lifetracker/cli run run categories create --parent 8 87 "Drugs" "Exploring the inner cosmos."
pnpm --filter=@lifetracker/cli run run categories create --parent 8 88 "Video games" "High-octane relaxation."
pnpm --filter=@lifetracker/cli run run categories create --parent 8 89 "Social media" "Scrolling."
pnpm --filter=@lifetracker/cli run run categories create 9 "Waste" "Time better spent doing something else." red
pnpm --filter=@lifetracker/cli run run categories create --parent 9 91 "Waiting / Killing time" "Literally staring at a clock with nothing else to do."
pnpm --filter=@lifetracker/cli run run categories create --parent 9 93 "Fight" "Not understanding or communicating well."
pnpm --filter=@lifetracker/cli run run categories create --parent 9 96 "Disaster" "Something has gone horribly wrong, and everything else is canceled."
pnpm --filter=@lifetracker/cli run run categories create --parent 9 97 "Shopping" "Ah, mindless consumerism!"
pnpm --filter=@lifetracker/cli run run categories create --parent 9 98 "Can't sleep" "Lying in bed wishing I was asleep."
pnpm --filter=@lifetracker/cli run run categories create --parent 9 99 "Stress eating / drinking / smoking" "Shoveling empty calories or toxins, knowing it's a mistake."
pnpm --filter=@lifetracker/cli run run categories create 10 "Health & Travel" "Time spent for personal hygiene, getting around, or similar." lime
pnpm --filter=@lifetracker/cli run run categories create --parent 10 101 "Food" "A regular ol' meal."
pnpm --filter=@lifetracker/cli run run categories create --parent 10 103 "Bath" "Taking a bath."
pnpm --filter=@lifetracker/cli run run categories create --parent 10 104 "Travel" "Getting from a place to another place."
pnpm --filter=@lifetracker/cli run run categories create --parent 10 105 "Bathroom" "Getting clean to go somewhere or do something -- or, flossing, brushing, and getting ready for bed.."
pnpm --filter=@lifetracker/cli run run categories create --parent 10 106 "Packing & Cleaning" "Packing for a trip and cleaning before or after a trip."
pnpm --filter=@lifetracker/cli run run categories create --parent 10 107 "Medical / Health" "A doctor, dentist, hospital visit, or appointment of some kind."
pnpm --filter=@lifetracker/cli run run categories create --parent 10 108 "Grooming / Massage" "Taking care of myself in a more aesthetic or relaxing way."

11
scripts/create_colors.sh Normal file
View File

@ -0,0 +1,11 @@
pnpm --filter=@lifetracker/cli run run colors create Black "#273036"
pnpm --filter=@lifetracker/cli run run colors create Blue "#00A9B3"
pnpm --filter=@lifetracker/cli run run colors create Lime "#BFFF55"
pnpm --filter=@lifetracker/cli run run colors create Green "#189749"
pnpm --filter=@lifetracker/cli run run colors create Pink "#FF65AE"
pnpm --filter=@lifetracker/cli run run colors create Purple "#5B3AB1"
pnpm --filter=@lifetracker/cli run run colors create Orange "#FF6D01"
pnpm --filter=@lifetracker/cli run run colors create Yellow "#FFF336"
pnpm --filter=@lifetracker/cli run run colors create Teal "#005744"
pnpm --filter=@lifetracker/cli run run colors create Crimson "#C71634"
pnpm --filter=@lifetracker/cli run run colors create Red "#FF2816"