Basic Day functionality works in CLI and Web (getOrCreate, update Mood, Comment)

This commit is contained in:
Ryan Pandya 2024-11-26 08:50:12 -08:00
parent d440eb155d
commit 4af1f35687
31 changed files with 1495 additions and 258 deletions

View File

@ -16,11 +16,13 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@date-fns/tz": "^1.2.0",
"@lifetracker/db": "workspace:*", "@lifetracker/db": "workspace:*",
"@lifetracker/trpc": "workspace:^", "@lifetracker/trpc": "workspace:^",
"date-fns": "^4.1.0",
"dotenv": "^16.4.1",
"table": "^6.8.2", "table": "^6.8.2",
"vite-tsconfig-paths": "^5.1.0", "vite-tsconfig-paths": "^5.1.0"
"dotenv": "^16.4.1"
}, },
"devDependencies": { "devDependencies": {
"@commander-js/extra-typings": "^12.1.0", "@commander-js/extra-typings": "^12.1.0",

View File

@ -0,0 +1,61 @@
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";
import { format } from "date-fns";
import { TZDate } from "@date-fns/tz";
function moodToStars(mood: number) {
// const full_stars = Math.floor(mood / 2);
// const half_star = mood % 2;
// return "★".repeat(full_stars) + (half_star ? "⯨" : "");
return "★".repeat(mood) + "☆".repeat(10 - mood);
}
export const daysCmd = new Command()
.name("day")
.description("Get data for a specific day")
.argument('<date>', 'A date in ISO-8601 format, or "yesterday", "today", "tomorrow", etc.')
.option('-c, --comment <comment>', "edit this day's comment")
.option('-m, --mood <number>', "edit this day's mood")
.action(async (dateQuery: string, flags?) => {
const api = getAPIClient();
try {
if (flags?.comment || flags?.mood) {
const updateProps = { dateQuery: dateQuery, ...flags };
await api.days.update.mutate(updateProps);
}
const day = (await api.days.get.query({ dateQuery: dateQuery }));
if (getGlobalOptions().json) {
printObject(day);
} else {
const moodStr = moodToStars(day.mood);
const dateStr = format(`${day.date}T00:00:00`, "EEEE, MMMM do");
const data: string[][] = [[dateStr, moodStr], [day.comment ?? "No comment", '',]];
console.log(table(data, {
// border: getBorderCharacters("ramac"),
// singleLine: true,
spanningCells: [{ col: 0, row: 1, colSpan: 2 }],
drawVerticalLine: (lineIndex, columnCount) => {
return lineIndex === 0 || lineIndex === columnCount || (lineIndex === 0 && columnCount === 2);
},
drawHorizontalLine: (lineIndex, rowCount) => {
return (lineIndex < 2 || lineIndex === rowCount);
},
}));
}
} catch (error) {
printErrorMessageWithReason("Failed to get day", error as object);
}
});

View File

@ -4,6 +4,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 { categoriesCmd } from "@/commands/categories";
import { daysCmd } from "@/commands/days";
import { config } from "dotenv"; import { config } from "dotenv";
@ -36,6 +37,7 @@ const program = new Command()
); );
program.addCommand(whoamiCmd); program.addCommand(whoamiCmd);
program.addCommand(daysCmd);
program.addCommand(colorsCmd); program.addCommand(colorsCmd);
program.addCommand(categoriesCmd); program.addCommand(categoriesCmd);

View File

@ -1,10 +0,0 @@
import React from "react";
import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
export default async function BookmarksPage() {
return (
<div>
<Bookmarks query={{ archived: false }} showEditorCard={true} />
</div>
);
}

View File

@ -0,0 +1,32 @@
import { notFound } from "next/navigation";
import { api } from "@/server/api/client";
import { TRPCError } from "@trpc/server";
import DayView from "@/components/dashboard/days/DayView";
import LoadingSpinner from "@/components/ui/spinner";
export default async function DayPage({
params,
}: {
params: { dateQuery: string };
}) {
let day;
try {
day = await api.days.get({ dateQuery: params.dateQuery });
} catch (e) {
if (e instanceof TRPCError) {
if (e.code == "NOT_FOUND") {
notFound();
}
}
throw e;
}
return (
params.dateQuery === undefined ?
<LoadingSpinner />
:
<DayView
day={day}
/>
);
}

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
export default async function TodayPage() { export default async function MainDayPage() {
return ( return (
<div> <div>
Hello from a logged in page! Hello from a logged in page!

View File

@ -4,7 +4,7 @@ import { getServerAuthSession } from "@/server/auth";
export default async function Home() { export default async function Home() {
const session = await getServerAuthSession(); const session = await getServerAuthSession();
if (session) { if (session) {
redirect("/dashboard/today"); redirect("/dashboard/day/today");
} else { } else {
redirect("/signin"); redirect("/signin");
} }

View File

@ -98,16 +98,14 @@ function ViewMode({
return ( return (
<Tooltip delayDuration={500}> <Tooltip delayDuration={500}>
<div className="flex max-w-full items-center gap-3"> <div className="flex max-w-full items-center gap-3">
<TooltipTrigger asChild> {originalText ? (
{originalText ? ( <p className={viewClassName}>{originalText}</p>
<p className={viewClassName}>{originalText}</p> ) : (
) : ( <p className={untitledClassName}>Untitled</p>
<p className={untitledClassName}>Untitled</p> )}
)}
</TooltipTrigger>
<ButtonWithTooltip <ButtonWithTooltip
delayDuration={500} delayDuration={500}
tooltip="Edit title" tooltip="Edit"
size="none" size="none"
variant="ghost" variant="ghost"
className="align-middle text-gray-400" className="align-middle text-gray-400"

View File

@ -1,146 +0,0 @@
"use client";
import React, { useCallback, useState } from "react";
import useUpload from "@/lib/hooks/upload-file";
import { cn } from "@/lib/utils";
import { TRPCClientError } from "@trpc/client";
import DropZone from "react-dropzone";
import { useCreateBookmarkWithPostHook } from "@hoarder/shared-react/hooks/bookmarks";
import { BookmarkTypes } from "@hoarder/shared/types/bookmarks";
import LoadingSpinner from "../ui/spinner";
import { toast } from "../ui/use-toast";
import BookmarkAlreadyExistsToast from "../utils/BookmarkAlreadyExistsToast";
export function useUploadAsset() {
const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook({
onSuccess: (resp) => {
if (resp.alreadyExists) {
toast({
description: <BookmarkAlreadyExistsToast bookmarkId={resp.id} />,
variant: "default",
});
} else {
toast({ description: "Bookmark uploaded" });
}
},
onError: () => {
toast({ description: "Something went wrong", variant: "destructive" });
},
});
const { mutateAsync: runUploadAsset } = useUpload({
onSuccess: async (resp) => {
const assetType =
resp.contentType === "application/pdf" ? "pdf" : "image";
await createBookmark({ ...resp, type: BookmarkTypes.ASSET, assetType });
},
onError: (err, req) => {
toast({
description: `${req.name}: ${err.error}`,
variant: "destructive",
});
},
});
return useCallback(
(file: File) => {
return runUploadAsset(file);
},
[runUploadAsset],
);
}
function useUploadAssets({
onFileUpload,
onFileError,
onAllUploaded,
}: {
onFileUpload: () => void;
onFileError: (name: string, e: Error) => void;
onAllUploaded: () => void;
}) {
const runUpload = useUploadAsset();
return async (files: File[]) => {
if (files.length == 0) {
return;
}
for (const file of files) {
try {
await runUpload(file);
onFileUpload();
} catch (e) {
if (e instanceof TRPCClientError || e instanceof Error) {
onFileError(file.name, e);
}
}
}
onAllUploaded();
};
}
export default function UploadDropzone({
children,
}: {
children: React.ReactNode;
}) {
const [numUploading, setNumUploading] = useState(0);
const [numUploaded, setNumUploaded] = useState(0);
const uploadAssets = useUploadAssets({
onFileUpload: () => {
setNumUploaded((c) => c + 1);
},
onFileError: () => {
setNumUploaded((c) => c + 1);
},
onAllUploaded: () => {
setNumUploading(0);
setNumUploaded(0);
return;
},
});
const [isDragging, setDragging] = useState(false);
const onDrop = (acceptedFiles: File[]) => {
uploadAssets(acceptedFiles);
setNumUploading(acceptedFiles.length);
setDragging(false);
};
return (
<DropZone
noClick
onDrop={onDrop}
onDragEnter={() => setDragging(true)}
onDragLeave={() => setDragging(false)}
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} hidden />
<div
className={cn(
"fixed inset-0 z-50 flex h-full w-full items-center justify-center bg-gray-200 opacity-90",
isDragging || numUploading > 0 ? undefined : "hidden",
)}
>
{numUploading > 0 ? (
<div className="flex items-center justify-center gap-2">
<p className="text-2xl font-bold text-gray-700">
Uploading {numUploaded} / {numUploading}
</p>
<LoadingSpinner />
</div>
) : (
<p className="text-2xl font-bold text-gray-700">
Drop Your Image / Bookmark file
</p>
)}
</div>
{children}
</div>
)}
</DropZone>
);
}

View File

@ -1,52 +0,0 @@
"use client";
import UploadDropzone from "@/components/dashboard/UploadDropzone";
import { api } from "@/lib/trpc";
import type {
ZGetBookmarksRequest,
ZGetBookmarksResponse,
} from "@hoarder/shared/types/bookmarks";
import { BookmarkGridContextProvider } from "@hoarder/shared-react/hooks/bookmark-grid-context";
import BookmarksGrid from "./BookmarksGrid";
export default function UpdatableBookmarksGrid({
query,
bookmarks: initialBookmarks,
showEditorCard = false,
}: {
query: ZGetBookmarksRequest;
bookmarks: ZGetBookmarksResponse;
showEditorCard?: boolean;
itemsPerPage?: number;
}) {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
api.bookmarks.getBookmarks.useInfiniteQuery(
{ ...query, useCursorV2: true },
{
initialData: () => ({
pages: [initialBookmarks],
pageParams: [query.cursor],
}),
initialCursor: null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
},
);
const grid = (
<BookmarksGrid
bookmarks={data!.pages.flatMap((b) => b.bookmarks)}
hasNextPage={hasNextPage}
fetchNextPage={() => fetchNextPage()}
isFetchingNextPage={isFetchingNextPage}
showEditorCard={showEditorCard}
/>
);
return (
<BookmarkGridContextProvider query={query}>
{showEditorCard ? <UploadDropzone>{grid}</UploadDropzone> : grid}
</BookmarkGridContextProvider>
);
}

View File

@ -0,0 +1,45 @@
import { redirect } from "next/navigation";
import { Separator } from "@/components/ui/separator";
import { api } from "@/server/api/client";
import { getServerAuthSession } from "@/server/auth";
import { ZDay } from "@lifetracker/shared/types/days";
import EditableDayComment from "./EditableDayComment";
import { format } from "date-fns";
export default async function DayView({
day,
header,
showDivider,
showEditorCard = false,
}: {
day: ZDay;
header?: React.ReactNode;
showDivider?: boolean;
showEditorCard?: boolean;
}) {
const session = await getServerAuthSession();
if (!session) {
redirect("/");
}
// const entries = await api.entries.get({ day: day });
return (
<div className="flex flex-col gap-3">
<div className="flex justify-between">
<span className="text-2xl">
{format(`${day.date}T00:00:00`, "EEEE, MMMM do")}
</span>
</div>
<Separator />
<div>Rating: {day.mood?.toString() ?? "-"}</div>
<EditableDayComment day={day}
className="text-xl"
/>
</div>
);
}

View File

@ -0,0 +1,56 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { toast } from "@/components/ui/use-toast";
import { cn } from "@/lib/utils";
import { useUpdateDay } from "@lifetracker/shared-react/hooks/days";
import { EditableText } from "../EditableText";
import { format } from "date-fns";
export default function EditableDayComment({
day,
className,
}: {
day: { id: string; date: string, comment: string };
className?: string;
}) {
const router = useRouter();
const currentPath = usePathname();
const { mutate: updateDay, isPending } = useUpdateDay({
onSuccess: () => {
toast({
description: "Day updated!",
});
if (currentPath.includes("dashboard")) {
router.refresh();
}
},
});
return (
<EditableText
viewClassName={className}
editClassName={cn("p-2", className)}
originalText={day.comment ?? "No Comment"}
onSave={(newComment) => {
if (!newComment) { return }
updateDay(
{
dateQuery: format(`${day.date}T00:00:00`, "yyyy-MM-dd"),
comment: newComment,
},
{
onError: (e) => {
console.log(e);
toast({
description: e.message,
variant: "destructive",
});
},
},
);
}}
isSaving={isPending}
/>
);
}

View File

@ -49,6 +49,7 @@
"clsx": "^2.1.0", "clsx": "^2.1.0",
"color-2-name": "^1.4.4", "color-2-name": "^1.4.4",
"csv-parse": "^5.5.6", "csv-parse": "^5.5.6",
"date-fns": "^4.1.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"drizzle-orm": "^0.33.0", "drizzle-orm": "^0.33.0",
"fastest-levenshtein": "^1.0.16", "fastest-levenshtein": "^1.0.16",

View File

@ -7,12 +7,10 @@ import path from "path";
import dbConfig from "./drizzle.config"; import dbConfig from "./drizzle.config";
console.log("dbURL: ", dbConfig.dbCredentials.url);
const sqlite = new Database(dbConfig.dbCredentials.url); const sqlite = new Database(dbConfig.dbCredentials.url);
export const db = drizzle(sqlite, { export const db = drizzle(sqlite, {
schema, schema,
logger: true logger: false
}); });
export function getInMemoryDB(runMigrations: boolean) { export function getInMemoryDB(runMigrations: boolean) {

View File

@ -54,10 +54,23 @@ CREATE TABLE `config` (
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE `day` ( CREATE TABLE `day` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `id` text PRIMARY KEY NOT NULL,
`mood` integer,
`date` text NOT NULL, `date` text NOT NULL,
`comment` text NOT NULL `mood` integer,
`comment` text
);
--> statement-breakpoint
CREATE TABLE `entry` (
`id` text PRIMARY KEY NOT NULL,
`createdAt` integer NOT NULL,
`userId` text NOT NULL,
`comment` text,
`dayId` integer,
`startedAt` integer NOT NULL,
`endedAt` integer NOT NULL,
`categoryId` text NOT NULL,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`categoryId`) REFERENCES `category`(`id`) ON UPDATE no action ON DELETE no action
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE `session` ( CREATE TABLE `session` (
@ -89,4 +102,5 @@ CREATE UNIQUE INDEX `apiKey_name_userId_unique` ON `apiKey` (`name`,`userId`);--
CREATE UNIQUE INDEX `category_userId_code_unique` ON `category` (`userId`,`code`);--> 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 `entry_userId_startedAt_endedAt_unique` ON `entry` (`userId`,`startedAt`,`endedAt`);--> 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": "7f1d6157-2405-4b96-be6a-a4bfb35f9aaf", "id": "ceed45ad-37d0-4e59-92bd-3be02d271535",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"account": { "account": {
@ -409,16 +409,9 @@
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "integer", "type": "text",
"primaryKey": true, "primaryKey": true,
"notNull": true, "notNull": true,
"autoincrement": true
},
"mood": {
"name": "mood",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false "autoincrement": false
}, },
"date": { "date": {
@ -428,11 +421,18 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"mood": {
"name": "mood",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"comment": { "comment": {
"name": "comment", "name": "comment",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false "autoincrement": false
} }
}, },
@ -449,6 +449,108 @@
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {} "uniqueConstraints": {}
}, },
"entry": {
"name": "entry",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"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
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"dayId": {
"name": "dayId",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"startedAt": {
"name": "startedAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"endedAt": {
"name": "endedAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"categoryId": {
"name": "categoryId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"entry_userId_startedAt_endedAt_unique": {
"name": "entry_userId_startedAt_endedAt_unique",
"columns": [
"userId",
"startedAt",
"endedAt"
],
"isUnique": true
}
},
"foreignKeys": {
"entry_userId_user_id_fk": {
"name": "entry_userId_user_id_fk",
"tableFrom": "entry",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"entry_categoryId_category_id_fk": {
"name": "entry_categoryId_category_id_fk",
"tableFrom": "entry",
"tableTo": "category",
"columnsFrom": [
"categoryId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"session": { "session": {
"name": "session", "name": "session",
"columns": { "columns": {

View File

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

View File

@ -1,6 +1,7 @@
import type { AdapterAccount } from "@auth/core/adapters"; import type { AdapterAccount } from "@auth/core/adapters";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { relations, SQL, sql } from "drizzle-orm"; import { relations, SQL, sql } from "drizzle-orm";
import { date } from "drizzle-orm/mysql-core";
import { import {
AnySQLiteColumn, AnySQLiteColumn,
index, index,
@ -122,10 +123,13 @@ export const verificationTokens = sqliteTable(
); );
export const days = sqliteTable("day", { export const days = sqliteTable("day", {
id: integer("id").primaryKey({ autoIncrement: true }), id: text("id")
mood: integer("mood"), .notNull()
.primaryKey()
.$defaultFn(() => createId()),
date: text("date").notNull().unique(), date: text("date").notNull().unique(),
comment: text("comment").notNull(), mood: integer("mood"),
comment: text("comment"),
}); });
export const colors = sqliteTable( export const colors = sqliteTable(
@ -175,6 +179,28 @@ export const categories = sqliteTable(
}), }),
); );
export const entries = sqliteTable(
"entry",
{
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => createId()),
createdAt: createdAtField(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
comment: text("comment"),
dayId: integer("dayId"),
startedAt: integer("startedAt", { mode: "timestamp" }).notNull(),
endedAt: integer("endedAt", { mode: "timestamp" }).notNull(),
categoryId: text("categoryId").notNull().references(() => categories.id),
},
(e) => ({
uniq: unique().on(e.userId, e.startedAt, e.endedAt)
}),
)
// Relations // Relations
export const apiKeyRelations = relations(apiKeys, ({ one }) => ({ export const apiKeyRelations = relations(apiKeys, ({ one }) => ({
@ -186,9 +212,21 @@ export const apiKeyRelations = relations(apiKeys, ({ one }) => ({
export const userRelations = relations(users, ({ many }) => ({ export const userRelations = relations(users, ({ many }) => ({
categories: many(categories), categories: many(categories),
colors: many(colors), colors: many(colors),
entries: many(entries),
})); }));
export const colorsRelations = relations(
colors,
({ many, one }) => ({
user: one(users, {
fields: [colors.userId],
references: [users.id],
}),
categories: many(categories),
}),
);
export const categoriesRelations = relations( export const categoriesRelations = relations(
categories, categories,
({ many, one }) => ({ ({ many, one }) => ({
@ -204,16 +242,32 @@ export const categoriesRelations = relations(
fields: [categories.parentId], fields: [categories.parentId],
references: [categories.id], references: [categories.id],
}), }),
entries: many(entries),
}), }),
); );
export const colorsRelations = relations( export const daysRelations = relations(
colors, days,
({ many, one }) => ({ ({ many }) => ({
user: one(users, { entries: many(entries),
fields: [colors.userId], }),
references: [users.id], );
}),
categories: many(categories), export const entriesRelations = relations(
entries,
({ many, one }) => ({
user: one(users, {
fields: [entries.userId],
references: [users.id],
}),
categories: one(categories, {
fields: [entries.categoryId],
references: [categories.id],
}
),
day: one(days, {
fields: [entries.dayId],
references: [days.id],
}),
}), }),
); );

View File

@ -0,0 +1,43 @@
import { api } from "../trpc";
export function useUpdateDay(
...opts: Parameters<typeof api.days.update.useMutation>
) {
const apiUtils = api.useUtils();
return api.days.update.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.days.get.invalidate({ dateQuery: req.dateQuery });
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
export function useDeleteLabel(
...opts: Parameters<typeof api.labels.delete.useMutation>
) {
const apiUtils = api.useUtils();
return api.labels.delete.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.labels.list.invalidate();
// apiUtils.bookmarks.getBookmark.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
export function useDeleteUnusedTags(
...opts: Parameters<typeof api.tags.deleteUnused.useMutation>
) {
const apiUtils = api.useUtils();
return api.tags.deleteUnused.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.tags.list.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}

View File

@ -8,6 +8,7 @@
"@lifetracker/trpc": "workspace:^", "@lifetracker/trpc": "workspace:^",
"@tanstack/react-query": "^5.24.8", "@tanstack/react-query": "^5.24.8",
"@trpc/client": "11.0.0-next-beta.308", "@trpc/client": "11.0.0-next-beta.308",
"@trpc/react-query": "11.0.0-next-beta.308",
"superjson": "^2.2.1" "superjson": "^2.2.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,9 @@
import { z } from "zod";
export const zDaySchema = z.object({
id: z.string().optional(),
date: z.string(),
mood: z.number().nullable(),
comment: z.string().nullable(),
});
export type ZDay = z.infer<typeof zDaySchema>;

View File

@ -56,19 +56,6 @@ function parseApiKey(plain: string) {
export async function authenticateApiKey(key: string) { export async function authenticateApiKey(key: string) {
const { keyId, keySecret } = parseApiKey(key); const { keyId, keySecret } = parseApiKey(key);
console.log("\n\nWELCOME TO HELL\n\n");
console.log(parseApiKey(key));
// Console.log all rows in the apiKeys table
console.log(await db.query.apiKeys.findMany());
console.log(await db.query.apiKeys.findFirst({
where: (k, { eq }) => eq(k.keyId, keyId),
with: {
user: true,
},
}));
console.log("\n\n\n\n");
const apiKey = await db.query.apiKeys.findFirst({ const apiKey = await db.query.apiKeys.findFirst({
where: (k, { eq }) => eq(k.keyId, keyId), where: (k, { eq }) => eq(k.keyId, keyId),
with: { with: {

View File

@ -10,11 +10,13 @@
"test": "vitest" "test": "vitest"
}, },
"dependencies": { "dependencies": {
"@date-fns/tz": "^1.2.0",
"@lifetracker/db": "workspace:*", "@lifetracker/db": "workspace:*",
"@lifetracker/shared": "workspace:^", "@lifetracker/shared": "workspace:^",
"@lifetracker/ui": "workspace:*", "@lifetracker/ui": "workspace:*",
"@trpc/server": "^11.0.0-next-beta.308", "@trpc/server": "^11.0.0-next-beta.308",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.33.0", "drizzle-orm": "^0.33.0",
"superjson": "^2.2.1", "superjson": "^2.2.1",
"tiny-invariant": "^1.3.3", "tiny-invariant": "^1.3.3",

View File

@ -5,11 +5,13 @@ import { apiKeysAppRouter } from "./apiKeys";
import { adminAppRouter } from "./admin"; import { adminAppRouter } from "./admin";
import { categoriesAppRouter } from "./categories"; import { categoriesAppRouter } from "./categories";
import { colorsAppRouter } from "./colors"; import { colorsAppRouter } from "./colors";
import { daysAppRouter } from "./days";
export const appRouter = router({ export const appRouter = router({
users: usersAppRouter, users: usersAppRouter,
apiKeys: apiKeysAppRouter, apiKeys: apiKeysAppRouter,
admin: adminAppRouter, admin: adminAppRouter,
days: daysAppRouter,
colors: colorsAppRouter, colors: colorsAppRouter,
categories: categoriesAppRouter, categories: categoriesAppRouter,
}); });

View File

@ -0,0 +1,105 @@
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 { days, } from "@lifetracker/db/schema";
import {
zDaySchema, ZDay
} from "@lifetracker/shared/types/days";
import type { Context } from "../index";
import { authedProcedure, router } from "../index";
import { format } from "date-fns";
import { TZDate } from "@date-fns/tz";
function dateFromInput(input: { dateQuery: string }) {
let t: string;
console.log(input.dateQuery);
if (input.dateQuery == "today") {
t = TZDate.tz("America/Los_Angeles");
}
else {
t = new TZDate(input.dateQuery, "Etc/UTC");
}
console.log(format(t, "yyyy-MM-dd"));
return format(t, "yyyy-MM-dd");
}
async function createDay(date: string, ctx: Context) {
return await ctx.db.transaction(async (trx) => {
try {
const result = await trx
.insert(days)
.values({
date: date,
})
.returning({
id: days.id,
date: days.date,
mood: days.mood,
comment: days.comment,
});
return result[0];
} catch (e) {
console.log(e);
if (e instanceof SqliteError) {
if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "This day already exists",
});
}
}
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Something went wrong",
});
}
});
}
export const daysAppRouter = router({
get: authedProcedure
.input(z.object({
dateQuery: z.string(),
}))
.output(zDaySchema)
.query(async ({ input, ctx }) => {
const date = dateFromInput(input);
const res = await ctx.db
.select({
id: days.id,
date: days.date,
mood: days.mood,
comment: days.comment,
})
.from(days)
.where(eq(days.date, date));
const day = res.length == 0 ? createDay(date, ctx) : res[0];
return day;
}),
update: authedProcedure
.input(
z.object({
mood: z.string().optional().or(z.number()),
comment: z.string().optional(),
dateQuery: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const { dateQuery, ...updatedProps } = input;
// Convert mood to number, if it exists
if (updatedProps.mood) {
updatedProps.mood = parseInt(updatedProps.mood);
}
console.log(dateQuery, "::", dateFromInput({ dateQuery: dateQuery }));
await ctx.db
.update(days)
.set(updatedProps)
.where(eq(days.date, dateFromInput({ dateQuery: dateQuery })));
}),
});

28
pnpm-lock.yaml generated
View File

@ -20,12 +20,18 @@ importers:
apps/cli: apps/cli:
dependencies: dependencies:
'@date-fns/tz':
specifier: ^1.2.0
version: 1.2.0
'@lifetracker/db': '@lifetracker/db':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/db version: link:../../packages/db
'@lifetracker/trpc': '@lifetracker/trpc':
specifier: workspace:^ specifier: workspace:^
version: link:../../packages/trpc version: link:../../packages/trpc
date-fns:
specifier: ^4.1.0
version: 4.1.0
dotenv: dotenv:
specifier: ^16.4.1 specifier: ^16.4.1
version: 16.4.5 version: 16.4.5
@ -235,6 +241,9 @@ importers:
csv-parse: csv-parse:
specifier: ^5.5.6 specifier: ^5.5.6
version: 5.5.6 version: 5.5.6
date-fns:
specifier: ^4.1.0
version: 4.1.0
dayjs: dayjs:
specifier: ^1.11.10 specifier: ^1.11.10
version: 1.11.13 version: 1.11.13
@ -482,6 +491,9 @@ importers:
'@trpc/client': '@trpc/client':
specifier: 11.0.0-next-beta.308 specifier: 11.0.0-next-beta.308
version: 11.0.0-next-beta.308(@trpc/server@11.0.0-next-beta.308) version: 11.0.0-next-beta.308(@trpc/server@11.0.0-next-beta.308)
'@trpc/react-query':
specifier: 11.0.0-next-beta.308
version: 11.0.0-next-beta.308(@tanstack/react-query@5.60.2(react@18.3.1))(@trpc/client@11.0.0-next-beta.308(@trpc/server@11.0.0-next-beta.308))(@trpc/server@11.0.0-next-beta.308)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: react:
specifier: '*' specifier: '*'
version: 18.3.1 version: 18.3.1
@ -544,6 +556,9 @@ importers:
packages/trpc: packages/trpc:
dependencies: dependencies:
'@date-fns/tz':
specifier: ^1.2.0
version: 1.2.0
'@lifetracker/db': '@lifetracker/db':
specifier: workspace:* specifier: workspace:*
version: link:../db version: link:../db
@ -559,6 +574,9 @@ importers:
bcryptjs: bcryptjs:
specifier: ^2.4.3 specifier: ^2.4.3
version: 2.4.3 version: 2.4.3
date-fns:
specifier: ^4.1.0
version: 4.1.0
drizzle-orm: drizzle-orm:
specifier: ^0.33.0 specifier: ^0.33.0
version: 0.33.0(@types/better-sqlite3@7.6.11)(@types/react@18.2.61)(better-sqlite3@11.5.0)(expo-sqlite@14.0.6(expo@51.0.36(@babel/core@7.26.0)(@babel/preset-env@7.25.7(@babel/core@7.26.0))))(react@18.3.1) version: 0.33.0(@types/better-sqlite3@7.6.11)(@types/react@18.2.61)(better-sqlite3@11.5.0)(expo-sqlite@14.0.6(expo@51.0.36(@babel/core@7.26.0)(@babel/preset-env@7.25.7(@babel/core@7.26.0))))(react@18.3.1)
@ -1896,6 +1914,9 @@ packages:
'@dabh/diagnostics@2.0.3': '@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
'@date-fns/tz@1.2.0':
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
'@discoveryjs/json-ext@0.5.7': '@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@ -5683,6 +5704,9 @@ packages:
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dayjs@1.11.13: dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@ -14179,6 +14203,8 @@ snapshots:
enabled: 2.0.0 enabled: 2.0.0
kuler: 2.0.0 kuler: 2.0.0
'@date-fns/tz@1.2.0': {}
'@discoveryjs/json-ext@0.5.7': {} '@discoveryjs/json-ext@0.5.7': {}
'@docsearch/css@3.6.2': {} '@docsearch/css@3.6.2': {}
@ -19106,6 +19132,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-data-view: 1.0.1 is-data-view: 1.0.1
date-fns@4.1.0: {}
dayjs@1.11.13: {} dayjs@1.11.13: {}
debounce@1.2.1: {} debounce@1.2.1: {}

10
scripts/apiKey.json Normal file
View File

@ -0,0 +1,10 @@
[
{
"createdAt": 1731634820,
"id": "wxl62dg2n721tgzlz3spnjbu",
"keyHash": "$2a$10$NhOG42FjMbDycWHcJI4LH.Jp.aCV.op7llIP0zw/CBUmB3lO0HHQu",
"keyId": "b9b17eb909ce0ecdbf33",
"name": "CLI App",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
}
]

792
scripts/category.json Normal file
View File

@ -0,0 +1,792 @@
[
{
"code": 0.0,
"colorId": "ej29qv1e7l1dnfxajl0561dt",
"createdAt": 1732442037,
"description": "Time spent sleeping.",
"id": "pjb9vypaflgdcl3w9cn59bfy",
"name": "Sleep",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 0.4,
"colorId": "ej29qv1e7l1dnfxajl0561dt",
"createdAt": 1732442768,
"description": "Typically shitty half-sleep on a plane or in a car.",
"id": "iazlvs3xxaml3viytgcisvbl",
"name": "Travel sleep",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 0.5,
"colorId": "ej29qv1e7l1dnfxajl0561dt",
"createdAt": 1732443778,
"description": "Sleep other than at night.",
"id": "orqt6lk4jyy0578e9vt3jn37",
"name": "Nap",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 0.9,
"colorId": "ej29qv1e7l1dnfxajl0561dt",
"createdAt": 1732443779,
"description": "Resting while sick or injured.",
"id": "qtfjtrpbbz7adlyd2ynn4tq6",
"name": "Sick",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 1.0,
"colorId": "ahapbp9ujym8lgf4ny02i26h",
"createdAt": 1732444271,
"description": "Time spent with family.",
"id": "iwja5v39r48weawdp5rmomb1",
"name": "Family",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 13.0,
"colorId": "ahapbp9ujym8lgf4ny02i26h",
"createdAt": 1732444273,
"description": "Wedding and related stuff.",
"id": "vwihwohm09c1zsoh6rnddw0m",
"name": "Wedding, etc.",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 15.0,
"colorId": "ahapbp9ujym8lgf4ny02i26h",
"createdAt": 1732444274,
"description": "Local family, if you will.",
"id": "ontipjpabhqx5wlocjab0xpd",
"name": "Neighborhood / Community",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 16.0,
"colorId": "ahapbp9ujym8lgf4ny02i26h",
"createdAt": 1732444275,
"description": "TV, movies, or the like, watched with family.",
"id": "e4hbzc6f3t43gfrbfj25m7ve",
"name": "Watching something",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 2.0,
"colorId": "wseq6pmjce562ds089pduv24",
"createdAt": 1732444277,
"description": "Time spent with friends.",
"id": "gozno3fockquztot23l7u62w",
"name": "Friends",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 22.0,
"colorId": "wseq6pmjce562ds089pduv24",
"createdAt": 1732444278,
"description": "Party with friends.",
"id": "on371jnjjof728m72hvmvvq5",
"name": "Party",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 26.0,
"colorId": "wseq6pmjce562ds089pduv24",
"createdAt": 1732444280,
"description": "Playing active games or sports with friends.",
"id": "uy1mexnou9phcm9usxujqcu6",
"name": "Active / Sports",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 28.0,
"colorId": "wseq6pmjce562ds089pduv24",
"createdAt": 1732444281,
"description": "Preparing or eating a meal with friends.",
"id": "mqamdgvll8fhpeh04iaahgt1",
"name": "Meal",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 29.0,
"colorId": "wseq6pmjce562ds089pduv24",
"createdAt": 1732444283,
"description": "Drugs with friends.",
"id": "jn5uzh2j2n4yj8faf1hq1m1h",
"name": "Drugs",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 3.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444284,
"description": "Time spent on dates or primarily with/for Jen.",
"id": "na3g6sz33dxgxd2p27pep4d8",
"name": "Jen",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 31.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444286,
"description": "Time spent with Jen's family.",
"id": "pw0ev83xwb7egvkjkwatjy0k",
"name": "Jen's family",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 32.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444287,
"description": "Time with primarily Jen's friends.",
"id": "aybqbkj4oigocfhtgf9xiziu",
"name": "Jen's friends",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 33.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444289,
"description": "Sex or sex-related activities.",
"id": "ocicgnndcpb2brdhi9hzj3jk",
"name": "Sex",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 34.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444290,
"description": "Exploring and adventuring, with a more urban (non-nature) focus.",
"id": "z07iadfzm0gdnuexeybl78tj",
"name": "Exploring",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 35.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444291,
"description": "When shit needs to get done, for me and Jen.",
"id": "ufdq763hkkv1a6fjjstsmkpg",
"name": "Errands & Logistics",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 36.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444293,
"description": "Watching a movie, TV show, or Internet stuff with Jen.",
"id": "pnqrj2ksxd86g2kv3pwfokjj",
"name": "Watching something",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 37.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444295,
"description": "Hiking, camping, and backcountry adventures with an active focus.",
"id": "pravf1muoqxvpg7pr590t882",
"name": "Hiking / Adventuring",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 38.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444296,
"description": "Cooking, eating out, or just having a meal with my love.",
"id": "uoaghsua2r5qtp6ef0nwil01",
"name": "Meal with Jen",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 39.0,
"colorId": "v86zsclb9cayijf8djilk5lv",
"createdAt": 1732444297,
"description": "Rest and cuddles, other than primarily for sleep or sex.",
"id": "nb70nsg3e3ch27t1o3ke24av",
"name": "Relaxing / Napping",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 4.0,
"colorId": "wtlivlqr3ehecyo9h76asydc",
"createdAt": 1732444299,
"description": "Anything related to private flying.",
"id": "k5pxee2j5qr80fa844x28xyk",
"name": "Flying",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 41.0,
"colorId": "wtlivlqr3ehecyo9h76asydc",
"createdAt": 1732444300,
"description": "Dealing with FBOs, getting in or out of the plane, etc.",
"id": "qcg39xfln3r1a2ae9d1h6iyu",
"name": "Airports, etc.",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 42.0,
"colorId": "wtlivlqr3ehecyo9h76asydc",
"createdAt": 1732444302,
"description": "Tour or flight for fun with friends.",
"id": "gendguvt1pbmc9ybgrhrr8sa",
"name": "Flying with friends",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 43.0,
"colorId": "wtlivlqr3ehecyo9h76asydc",
"createdAt": 1732444303,
"description": "A flying trip primarily for me and Jen.",
"id": "lo5fsd43de6rkf3fia3xsjmu",
"name": "Flying with Jen",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 44.0,
"colorId": "wtlivlqr3ehecyo9h76asydc",
"createdAt": 1732444305,
"description": "Checking weather, planning, filing flight plan, etc.",
"id": "jepjh7y5y49c8f8araawnyzr",
"name": "Flight planning",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 45.0,
"colorId": "wtlivlqr3ehecyo9h76asydc",
"createdAt": 1732444306,
"description": "A primarily business-oriented flying trip.",
"id": "wm210bl3mdfxzwimxk0hrxj3",
"name": "Flying for work",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 46.0,
"colorId": "wtlivlqr3ehecyo9h76asydc",
"createdAt": 1732444307,
"description": "A primarily maintenance-related flying trip.",
"id": "dhjwzwur71t7y4nvil2f97l6",
"name": "Maintenance",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 47.0,
"colorId": "wtlivlqr3ehecyo9h76asydc",
"createdAt": 1732444309,
"description": "A primarily training-related flying trip.",
"id": "gmdiu72n8q9u5nc8273ej76c",
"name": "Training",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 49.0,
"colorId": "wtlivlqr3ehecyo9h76asydc",
"createdAt": 1732444310,
"description": "e.g., Phone calls, research, buying/selling",
"id": "lm2xe9ouzo07ewlfpiu3xl74",
"name": "Other Flying Related",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 5.0,
"colorId": "nnpn5rrn3wx47i1553tvx8tb",
"createdAt": 1732444312,
"description": "Time spent working for money.",
"id": "bwnzw4g3dm1qns7cw0h1gwt1",
"name": "Work",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 51.0,
"colorId": "nnpn5rrn3wx47i1553tvx8tb",
"createdAt": 1732444313,
"description": "Pitching, diligence, calls, or somehow convincing investors to pony up.",
"id": "k3eofqjexruoklmqys5pndq9",
"name": "Investors",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 52.0,
"colorId": "nnpn5rrn3wx47i1553tvx8tb",
"createdAt": 1732444315,
"description": "Meeting with one or more of Perumal, Bonney, TM.",
"id": "veb5xubue47hqhebyzt3jlaa",
"name": "Cofounders",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 53.0,
"colorId": "nnpn5rrn3wx47i1553tvx8tb",
"createdAt": 1732444316,
"description": "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.",
"id": "saun6t9fo6js9vfg0jta6mcd",
"name": "[REMAP ME] Wedding prep",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 55.0,
"colorId": "nnpn5rrn3wx47i1553tvx8tb",
"createdAt": 1732444318,
"description": "Dealing with Board members or Board matters.",
"id": "lyad7iipkvwefn9xdd2mn48e",
"name": "Board",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 56.0,
"colorId": "nnpn5rrn3wx47i1553tvx8tb",
"createdAt": 1732444319,
"description": "Getting the inbox under control.",
"id": "zm2zwf7h29qz6py1i2fo225z",
"name": "Emails",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 57.0,
"colorId": "nnpn5rrn3wx47i1553tvx8tb",
"createdAt": 1732444321,
"description": "Thinking, ruminating, or journaling about work.",
"id": "gfxtjd5sae6d8i0y7rsmv3gv",
"name": "Work meditation",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 58.0,
"colorId": "nnpn5rrn3wx47i1553tvx8tb",
"createdAt": 1732444322,
"description": "Out building work-related relationships.",
"id": "gsbbgkm2um4vg30rs4dsmhlb",
"name": "Socializing",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 6.0,
"colorId": "y6hn5dkk8jfixg2nf11k5rvd",
"createdAt": 1732444323,
"description": "Time spent active (working out), fulfilling obligations, running errands, doing work without pay, or otherwise in a useful and productive way.",
"id": "gjfckt52lo0j5ucagp6r5ykp",
"name": "Productive",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 61.0,
"colorId": "y6hn5dkk8jfixg2nf11k5rvd",
"createdAt": 1732444325,
"description": "Tidying, organizing, or cleaning a space.",
"id": "cdk4v2mcer9cvtmsd51pwfc6",
"name": "Cleaning",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 62.0,
"colorId": "y6hn5dkk8jfixg2nf11k5rvd",
"createdAt": 1732444327,
"description": "Having important conversations or working on the big stuff.",
"id": "sn705vdodh4uy4lwxtua656o",
"name": "Relationship work",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 63.0,
"colorId": "y6hn5dkk8jfixg2nf11k5rvd",
"createdAt": 1732444329,
"description": "Productivity related to the home or household.",
"id": "zgxrgvqzy27kjdm3eoq2842b",
"name": "Home maintenance",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 65.0,
"colorId": "y6hn5dkk8jfixg2nf11k5rvd",
"createdAt": 1732444330,
"description": "Chores related to life administration and personal finances.",
"id": "l22sh0he7xj2rdlgb55l2iag",
"name": "Personal finances / Life admin",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 66.0,
"colorId": "y6hn5dkk8jfixg2nf11k5rvd",
"createdAt": 1732444332,
"description": "Walking, running, biking, or hiking outdoors.",
"id": "em9t97vltn1sdikap2okjiw6",
"name": "Outdoor exercise",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 67.0,
"colorId": "y6hn5dkk8jfixg2nf11k5rvd",
"createdAt": 1732444333,
"description": "Working on my fitness in a gym.",
"id": "vvs26xkohcnt8sroal5l0edc",
"name": "Gym",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 68.0,
"colorId": "y6hn5dkk8jfixg2nf11k5rvd",
"createdAt": 1732444335,
"description": "Working on myself.",
"id": "w2666f0qdo0fk0q9fjiglwbi",
"name": "Meditation / Therapy",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 69.0,
"colorId": "y6hn5dkk8jfixg2nf11k5rvd",
"createdAt": 1732444336,
"description": "Working on a needed but Kafkaesque nightmare chore.",
"id": "t9sxvt97kgkkfehxcuin1t4a",
"name": "Bureaucracy / Bullshit",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 7.0,
"colorId": "v0tzvsbxs1wq66123qy71r3o",
"createdAt": 1732444338,
"description": "Time used for personal development, improving skills, or what I consider to be productive hobbies.",
"id": "w42zyrh33ll3g3is563unhqx",
"name": "Hobbies & Skills",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 71.0,
"colorId": "v0tzvsbxs1wq66123qy71r3o",
"createdAt": 1732444339,
"description": "Learning, studying, reading, or practicing a foreign language.",
"id": "d03wwnvt9nnkeoz18ouxpdgo",
"name": "Language learning",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 72.0,
"colorId": "v0tzvsbxs1wq66123qy71r3o",
"createdAt": 1732444341,
"description": "Preparing food.",
"id": "grgvf8su5ms01tstj3jmcu2z",
"name": "Cooking",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 73.0,
"colorId": "v0tzvsbxs1wq66123qy71r3o",
"createdAt": 1732444342,
"description": "Fun or useful stuff for the house, including gardening, automation, or other projecs that are basically hobbies.",
"id": "adwj4mh49cqsj56muc1dhdal",
"name": "Home improvement",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 75.0,
"colorId": "v0tzvsbxs1wq66123qy71r3o",
"createdAt": 1732444344,
"description": "Reading an article, book, or something else edifying.",
"id": "csvjptm37vafhlp9vumc0vi0",
"name": "Reading",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 76.0,
"colorId": "v0tzvsbxs1wq66123qy71r3o",
"createdAt": 1732444345,
"description": "Working on a hobby or skill, but doing the boring / annoying bits.",
"id": "hzir2qmsy1fpww2fmi4ilxwf",
"name": "Organizing / Workflow",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 78.0,
"colorId": "v0tzvsbxs1wq66123qy71r3o",
"createdAt": 1732444347,
"description": "Nerding out and building something with computers.",
"id": "v4b02ymp61afkf6sxbi75a69",
"name": "Programming / Computers",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 79.0,
"colorId": "v0tzvsbxs1wq66123qy71r3o",
"createdAt": 1732444349,
"description": "Shopping that isn't a waste of time.",
"id": "gxel0vj9vdf9lxl4oz5k8bt6",
"name": "Shopping",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 8.0,
"colorId": "bmt6w43t0jd2s9x6igevhnmt",
"createdAt": 1732444350,
"description": "Time spent gaming, relaxing, consuming entertainment, or participating in passive activities.",
"id": "c5qffeg64wcsu4b2ow35pb5z",
"name": "Relaxation & Leisure",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 81.0,
"colorId": "bmt6w43t0jd2s9x6igevhnmt",
"createdAt": 1732444352,
"description": "Watching something produced for the screen (TV / Movies / Tiktok / YouTube)",
"id": "j7a54hdrm59e7h0kx4wjcu09",
"name": "Watching something",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 82.0,
"colorId": "bmt6w43t0jd2s9x6igevhnmt",
"createdAt": 1732444353,
"description": "Sweating or swimming in a relaxing way, other than to get clean.",
"id": "cauh6fv06sqj054idsc7os9e",
"name": "Hot tub / Sauna / Pool",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 83.0,
"colorId": "bmt6w43t0jd2s9x6igevhnmt",
"createdAt": 1732444355,
"description": "Self sex.",
"id": "cr1ddtrq7ut0wo8fgafcis1u",
"name": "Masturbation",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 87.0,
"colorId": "bmt6w43t0jd2s9x6igevhnmt",
"createdAt": 1732444356,
"description": "Exploring the inner cosmos.",
"id": "nal715gx0vw2jzrbi66keqpd",
"name": "Drugs",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 88.0,
"colorId": "bmt6w43t0jd2s9x6igevhnmt",
"createdAt": 1732444358,
"description": "High-octane relaxation.",
"id": "ool44khvisy404vzmkjo4bse",
"name": "Video games",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 89.0,
"colorId": "bmt6w43t0jd2s9x6igevhnmt",
"createdAt": 1732444359,
"description": "Scrolling.",
"id": "rz1a12cxs7oe097vdajtp462",
"name": "Social media",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 9.0,
"colorId": "lx36kwpg92wwtjfouxk5ha69",
"createdAt": 1732444361,
"description": "Time better spent doing something else.",
"id": "hsibd24jewb7rkyc68hvq2bo",
"name": "Waste",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 91.0,
"colorId": "lx36kwpg92wwtjfouxk5ha69",
"createdAt": 1732444362,
"description": "Literally staring at a clock with nothing else to do.",
"id": "zdxm0t6h86uw22mhecotcw1a",
"name": "Waiting / Killing time",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 93.0,
"colorId": "lx36kwpg92wwtjfouxk5ha69",
"createdAt": 1732444364,
"description": "Not understanding or communicating well.",
"id": "j5vt0dmzaomk859puypttx6f",
"name": "Fight",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 96.0,
"colorId": "lx36kwpg92wwtjfouxk5ha69",
"createdAt": 1732444365,
"description": "Something has gone horribly wrong, and everything else is canceled.",
"id": "i9q43q6ek9js8o5hok2b7ws1",
"name": "Disaster",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 97.0,
"colorId": "lx36kwpg92wwtjfouxk5ha69",
"createdAt": 1732444366,
"description": "Ah, mindless consumerism!",
"id": "nr9gqgr0seh6y8bvzm6zva1y",
"name": "Shopping",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 98.0,
"colorId": "lx36kwpg92wwtjfouxk5ha69",
"createdAt": 1732444368,
"description": "Lying in bed wishing I was asleep.",
"id": "jquru1u59f00gdapixmol2v6",
"name": "Can't sleep",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 99.0,
"colorId": "lx36kwpg92wwtjfouxk5ha69",
"createdAt": 1732444369,
"description": "Shoveling empty calories or toxins, knowing it's a mistake.",
"id": "z3gt25btq1ejib3ygv1aoq28",
"name": "Stress eating / drinking / smoking",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 10.0,
"colorId": "rk6d9osfs7hvjjofwh7a3tl9",
"createdAt": 1732444371,
"description": "Time spent for personal hygiene, getting around, or similar.",
"id": "z37g5owwy3jqlxv7ebvrl4qt",
"name": "Health & Travel",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 101.0,
"colorId": "rk6d9osfs7hvjjofwh7a3tl9",
"createdAt": 1732444372,
"description": "A regular ol' meal.",
"id": "h8xi5wefhl7gklh31j34d21l",
"name": "Food",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 103.0,
"colorId": "rk6d9osfs7hvjjofwh7a3tl9",
"createdAt": 1732444373,
"description": "Taking a bath.",
"id": "udix1ejz8zg7axmn56lh7tum",
"name": "Bath",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 104.0,
"colorId": "rk6d9osfs7hvjjofwh7a3tl9",
"createdAt": 1732444375,
"description": "Getting from a place to another place.",
"id": "bx9klccq3dxngm1ofq8lqggd",
"name": "Travel",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 105.0,
"colorId": "rk6d9osfs7hvjjofwh7a3tl9",
"createdAt": 1732444376,
"description": "Getting clean to go somewhere or do something -- or, flossing, brushing, and getting ready for bed..",
"id": "yjwfwupm3bzriffhumy3zdvt",
"name": "Bathroom",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 106.0,
"colorId": "rk6d9osfs7hvjjofwh7a3tl9",
"createdAt": 1732444378,
"description": "Packing for a trip and cleaning before or after a trip.",
"id": "r6288qk2icwl40anm9614dkx",
"name": "Packing & Cleaning",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 107.0,
"colorId": "rk6d9osfs7hvjjofwh7a3tl9",
"createdAt": 1732444379,
"description": "A doctor, dentist, hospital visit, or appointment of some kind.",
"id": "vn1xlghxpah2029ab1w15eua",
"name": "Medical / Health",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"code": 108.0,
"colorId": "rk6d9osfs7hvjjofwh7a3tl9",
"createdAt": 1732444380,
"description": "Taking care of myself in a more aesthetic or relaxing way.",
"id": "sk9z8c8uxw28lkqllx3n1e5h",
"name": "Grooming / Massage",
"parentId": null,
"userId": "n7yns1s5b122y5e8v0p6uyt8"
}
]

90
scripts/color.json Normal file
View File

@ -0,0 +1,90 @@
[
{
"createdAt": 1732441996,
"hexcode": "#273036",
"id": "ej29qv1e7l1dnfxajl0561dt",
"inverse": "#ffffff",
"name": "Black",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732441997,
"hexcode": "#00A9B3",
"id": "wtlivlqr3ehecyo9h76asydc",
"inverse": "#ffffff",
"name": "Blue",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732441999,
"hexcode": "#BFFF55",
"id": "rk6d9osfs7hvjjofwh7a3tl9",
"inverse": "#000000",
"name": "Lime",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732442000,
"hexcode": "#189749",
"id": "nnpn5rrn3wx47i1553tvx8tb",
"inverse": "#ffffff",
"name": "Green",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732442002,
"hexcode": "#FF65AE",
"id": "v86zsclb9cayijf8djilk5lv",
"inverse": "#ffffff",
"name": "Pink",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732442003,
"hexcode": "#5B3AB1",
"id": "bmt6w43t0jd2s9x6igevhnmt",
"inverse": "#ffffff",
"name": "Purple",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732442004,
"hexcode": "#FF6D01",
"id": "y6hn5dkk8jfixg2nf11k5rvd",
"inverse": "#ffffff",
"name": "Orange",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732442006,
"hexcode": "#FFF336",
"id": "v0tzvsbxs1wq66123qy71r3o",
"inverse": "#000000",
"name": "Yellow",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732442007,
"hexcode": "#005744",
"id": "wseq6pmjce562ds089pduv24",
"inverse": "#ffffff",
"name": "Teal",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732442008,
"hexcode": "#C71634",
"id": "ahapbp9ujym8lgf4ny02i26h",
"inverse": "#ffffff",
"name": "Crimson",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
},
{
"createdAt": 1732442010,
"hexcode": "#FF2816",
"id": "lx36kwpg92wwtjfouxk5ha69",
"inverse": "#ffffff",
"name": "Red",
"userId": "n7yns1s5b122y5e8v0p6uyt8"
}
]

11
scripts/user.json Normal file
View File

@ -0,0 +1,11 @@
[
{
"email": "ryan@ryanpandya.com",
"emailVerified": null,
"id": "n7yns1s5b122y5e8v0p6uyt8",
"image": null,
"name": "Ryan Pandya",
"password": "$2a$10$ngv9752uxDT11hSPfdZmAe2D8VXLB9mcXkN7TRPI5GZQCuriIu1gC",
"role": "admin"
}
]