From e648370637f141c21efd19a80924c47a15f181c7 Mon Sep 17 00:00:00 2001 From: Konstantin Rybakov Date: Wed, 12 Jun 2024 08:27:04 +0300 Subject: [PATCH] Konstantin/rka 21 add job actions (#5) * rka-21: move files around * rka-21: add job card actions * rka-21: clean up layout a bit * rka-21: add husky pre-commits and fix types --- .husky/pre-commit | 1 + .../company/actions/create-company.ts | 0 .../{ => components}/company/company-data.tsx | 3 +- .../variant/actions/check-url.ts | 0 app/add/{ => components}/variant/picker.tsx | 2 +- app/add/{ => components}/variant/renderer.tsx | 2 +- app/add/{ => components}/variant/types.ts | 0 .../{ => components}/variant/variants/url.tsx | 2 +- app/add/page.tsx | 6 +- app/add/state.ts | 2 +- app/components/list/action-button.tsx | 32 +++ app/components/list/actions/create-action.ts | 31 +++ app/components/list/actions/mark-property.ts | 11 + app/components/list/hooks/use-job-action.ts | 34 +++ app/components/list/job-card-actions.tsx | 53 ++++ app/{ => components/list}/job-card.tsx | 8 +- app/{ => components/list}/job-list.tsx | 0 app/{ => components/nav}/navigation.tsx | 0 app/layout.tsx | 4 +- app/page.tsx | 35 ++- bun.lockb | Bin 110721 -> 111068 bytes drizzle/0007_ancient_lady_bullseye.sql | 4 + drizzle/meta/0007_snapshot.json | 232 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + lib/db/queries.ts | 5 +- lib/db/queries/job-action.ts | 33 +++ lib/db/schema.ts | 15 +- package.json | 4 +- 28 files changed, 491 insertions(+), 35 deletions(-) create mode 100644 .husky/pre-commit rename app/add/{ => components}/company/actions/create-company.ts (100%) rename app/add/{ => components}/company/company-data.tsx (97%) rename app/add/{ => components}/variant/actions/check-url.ts (100%) rename app/add/{ => components}/variant/picker.tsx (95%) rename app/add/{ => components}/variant/renderer.tsx (86%) rename app/add/{ => components}/variant/types.ts (100%) rename app/add/{ => components}/variant/variants/url.tsx (98%) create mode 100644 app/components/list/action-button.tsx create mode 100644 app/components/list/actions/create-action.ts create mode 100644 app/components/list/actions/mark-property.ts create mode 100644 app/components/list/hooks/use-job-action.ts create mode 100644 app/components/list/job-card-actions.tsx rename app/{ => components/list}/job-card.tsx (86%) rename app/{ => components/list}/job-list.tsx (100%) rename app/{ => components/nav}/navigation.tsx (100%) create mode 100644 drizzle/0007_ancient_lady_bullseye.sql create mode 100644 drizzle/meta/0007_snapshot.json create mode 100644 lib/db/queries/job-action.ts diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..221fcfc --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +bun typecheck diff --git a/app/add/company/actions/create-company.ts b/app/add/components/company/actions/create-company.ts similarity index 100% rename from app/add/company/actions/create-company.ts rename to app/add/components/company/actions/create-company.ts diff --git a/app/add/company/company-data.tsx b/app/add/components/company/company-data.tsx similarity index 97% rename from app/add/company/company-data.tsx rename to app/add/components/company/company-data.tsx index 7105e6a..7f4c616 100644 --- a/app/add/company/company-data.tsx +++ b/app/add/components/company/company-data.tsx @@ -6,7 +6,8 @@ import { Box, Button, Flex, Text, TextField } from '@radix-ui/themes' import { useAtomValue } from 'jotai' import { useForm } from 'react-hook-form' import { z } from 'zod' -import { platformAtom, trackerURLAtom } from '../state' + +import { platformAtom, trackerURLAtom } from '../../state' import { actionCreateCompany } from './actions/create-company' const schema = z.object({ diff --git a/app/add/variant/actions/check-url.ts b/app/add/components/variant/actions/check-url.ts similarity index 100% rename from app/add/variant/actions/check-url.ts rename to app/add/components/variant/actions/check-url.ts diff --git a/app/add/variant/picker.tsx b/app/add/components/variant/picker.tsx similarity index 95% rename from app/add/variant/picker.tsx rename to app/add/components/variant/picker.tsx index 793a7ac..167df10 100644 --- a/app/add/variant/picker.tsx +++ b/app/add/components/variant/picker.tsx @@ -3,7 +3,7 @@ import { Flex, RadioCards, Text } from '@radix-ui/themes' import { useAtom } from 'jotai' -import { variantAtom } from '../state' +import { variantAtom } from '../../state' import type { Variant } from './types' export const VariantPicker = () => { diff --git a/app/add/variant/renderer.tsx b/app/add/components/variant/renderer.tsx similarity index 86% rename from app/add/variant/renderer.tsx rename to app/add/components/variant/renderer.tsx index 9b1873d..b6da55b 100644 --- a/app/add/variant/renderer.tsx +++ b/app/add/components/variant/renderer.tsx @@ -1,7 +1,7 @@ 'use client' import { useAtomValue } from 'jotai' -import { variantAtom } from '../state' +import { variantAtom } from '../../state' import { VariantURL } from './variants/url' // TODO: Rethink the renderer diff --git a/app/add/variant/types.ts b/app/add/components/variant/types.ts similarity index 100% rename from app/add/variant/types.ts rename to app/add/components/variant/types.ts diff --git a/app/add/variant/variants/url.tsx b/app/add/components/variant/variants/url.tsx similarity index 98% rename from app/add/variant/variants/url.tsx rename to app/add/components/variant/variants/url.tsx index a7ec511..308052d 100644 --- a/app/add/variant/variants/url.tsx +++ b/app/add/components/variant/variants/url.tsx @@ -14,7 +14,7 @@ import { useAtom, useSetAtom } from 'jotai' import Image from 'next/image' import { useForm } from 'react-hook-form' import { z } from 'zod' -import { platformAtom, trackerURLAtom } from '../../state' +import { platformAtom, trackerURLAtom } from '../../../state' import { actionCheckURL } from '../actions/check-url' const schema = z.object({ diff --git a/app/add/page.tsx b/app/add/page.tsx index a96f0ae..af3864c 100644 --- a/app/add/page.tsx +++ b/app/add/page.tsx @@ -4,9 +4,9 @@ import { isAdmin } from '@/lib/auth/is-admin' import { type User, currentUser } from '@clerk/nextjs/server' import { Provider } from 'jotai' import { redirect } from 'next/navigation' -import { CompanyName } from './company/company-data' -import { VariantPicker } from './variant/picker' -import { VariantRenderer } from './variant/renderer' +import { CompanyName } from './components/company/company-data' +import { VariantPicker } from './components/variant/picker' +import { VariantRenderer } from './components/variant/renderer' export default async function Add() { const user = (await currentUser()) as User diff --git a/app/add/state.ts b/app/add/state.ts index 0d76d2e..e4b577c 100644 --- a/app/add/state.ts +++ b/app/add/state.ts @@ -1,6 +1,6 @@ import type { HiringPlatformName } from '@/lib/db/schema' import { atom } from 'jotai' -import type { Variant } from './variant/types' +import type { Variant } from './components/variant/types' export const variantAtom = atom('url') export const platformAtom = atom(null) diff --git a/app/components/list/action-button.tsx b/app/components/list/action-button.tsx new file mode 100644 index 0000000..5927fda --- /dev/null +++ b/app/components/list/action-button.tsx @@ -0,0 +1,32 @@ +import { Button } from '@radix-ui/themes' +import type { BaseButtonProps } from '@radix-ui/themes/dist/esm/components/base-button.js' +import type { ReactNode } from 'react' + +type ActionButtonProps = { + loading: boolean + clickHandler: () => void + isActive: boolean + colorActive: BaseButtonProps['color'] + icon: ReactNode + label: string +} + +export const ActionButton = ({ + loading, + clickHandler, + isActive, + colorActive, + icon, + label, +}: ActionButtonProps) => ( + +) diff --git a/app/components/list/actions/create-action.ts b/app/components/list/actions/create-action.ts new file mode 100644 index 0000000..234da71 --- /dev/null +++ b/app/components/list/actions/create-action.ts @@ -0,0 +1,31 @@ +import type { SelectJob } from '@/lib/db/schema' +import { logger } from '@/lib/logger' +import type { ActionResponse } from '@/lib/types/api' + +export const createAction = + (query: (jobId: SelectJob['id'], property: boolean) => Promise) => + async ( + jobId: SelectJob['id'], + property: boolean, + ): Promise> => { + const l = logger.child({ jobId, property }) + + try { + const result = await query(jobId, property) + + return { + data: result, + error: false, + } + } catch (error) { + l.error(error) + + return { + error: true, + errorMessage: + error instanceof Error + ? error.message + : '[createAction] Unknown error', + } + } + } diff --git a/app/components/list/actions/mark-property.ts b/app/components/list/actions/mark-property.ts new file mode 100644 index 0000000..e640c4c --- /dev/null +++ b/app/components/list/actions/mark-property.ts @@ -0,0 +1,11 @@ +'use server' +import { + queryMarkHidden, + queryMarkSeen, + queryMarkTopChoice, +} from '@/lib/db/queries/job-action' +import { createAction } from './create-action' + +export const actionMarkTopChoice = createAction(queryMarkTopChoice) +export const actionMarkHidden = createAction(queryMarkHidden) +export const actionMarkSeen = createAction(queryMarkSeen) diff --git a/app/components/list/hooks/use-job-action.ts b/app/components/list/hooks/use-job-action.ts new file mode 100644 index 0000000..880024a --- /dev/null +++ b/app/components/list/hooks/use-job-action.ts @@ -0,0 +1,34 @@ +import type { SelectJob } from '@/lib/db/schema' +import { useState } from 'react' +import type { createAction } from '../actions/create-action' + +export const useJobAction = ( + initialState: boolean, + action: ReturnType, +) => { + const [isActive, setIsActive] = useState(initialState) + const [loading, setLoading] = useState(false) + + const clickHandler = async (jobId: SelectJob['id']) => { + const newState = !isActive + + setLoading(true) + + try { + const result = await action(jobId, newState) + + if (result.error) { + // TODO: Add toast notifications + console.error(result.errorMessage) + } else { + setIsActive(newState) + } + } catch (error) { + console.error(error) + } finally { + setLoading(false) + } + } + + return { isActive, loading, clickHandler } +} diff --git a/app/components/list/job-card-actions.tsx b/app/components/list/job-card-actions.tsx new file mode 100644 index 0000000..b0a9a97 --- /dev/null +++ b/app/components/list/job-card-actions.tsx @@ -0,0 +1,53 @@ +'use client' +import type { SelectJob } from '@/lib/db/schema' +import { + EyeNoneIcon, + EyeOpenIcon, + LightningBoltIcon, +} from '@radix-ui/react-icons' +import { Text } from '@radix-ui/themes' +import { ActionButton } from './action-button' +import { + actionMarkHidden, + actionMarkSeen, + actionMarkTopChoice, +} from './actions/mark-property' +import { useJobAction } from './hooks/use-job-action' + +export const JobCardActions = ({ job }: { job: SelectJob }) => { + const hidden = useJobAction(job.isHidden, actionMarkHidden) + const topChoice = useJobAction(job.isTopChoice, actionMarkTopChoice) + const seen = useJobAction(job.isSeen, actionMarkSeen) + + return ( + <> + + Mark as: + + hidden.clickHandler(job.id)} + colorActive="sky" + icon={} + label="Hidden" + /> + topChoice.clickHandler(job.id)} + colorActive="plum" + icon={} + label="Top Choice" + /> + seen.clickHandler(job.id)} + colorActive="grass" + icon={} + label="Seen" + /> + + ) +} diff --git a/app/job-card.tsx b/app/components/list/job-card.tsx similarity index 86% rename from app/job-card.tsx rename to app/components/list/job-card.tsx index 8a81f47..f6cca64 100644 --- a/app/job-card.tsx +++ b/app/components/list/job-card.tsx @@ -12,6 +12,7 @@ import { Text, } from '@radix-ui/themes' import NextLink from 'next/link' +import { JobCardActions } from './job-card-actions' type JobCardProps = { job: Awaited[number] @@ -21,8 +22,8 @@ type JobCardProps = { export const JobCard = async ({ job }: JobCardProps) => { return ( - - + + {job.title} @@ -53,6 +54,9 @@ export const JobCard = async ({ job }: JobCardProps) => { }).format(new Date(job.lastUpdatedAt))} + + + ) diff --git a/app/job-list.tsx b/app/components/list/job-list.tsx similarity index 100% rename from app/job-list.tsx rename to app/components/list/job-list.tsx diff --git a/app/navigation.tsx b/app/components/nav/navigation.tsx similarity index 100% rename from app/navigation.tsx rename to app/components/nav/navigation.tsx diff --git a/app/layout.tsx b/app/layout.tsx index 5e9a880..5de7acf 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,7 +9,7 @@ import { Box, Container, Flex, Section, Theme } from '@radix-ui/themes' import type { Metadata } from 'next' import '@radix-ui/themes/styles.css' -import { Navigation } from './navigation' +import { Navigation } from './components/nav/navigation' export const metadata: Metadata = { title: 'OWAT!', @@ -28,7 +28,7 @@ export default function RootLayout({
- + diff --git a/app/page.tsx b/app/page.tsx index c352135..39e4393 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,9 +1,9 @@ import { isAdmin } from '@/lib/auth/is-admin' import { type User, currentUser } from '@clerk/nextjs/server' import { PlusIcon } from '@radix-ui/react-icons' -import { Box, Button, Heading, Section } from '@radix-ui/themes' +import { Box, Button, Flex, Heading, Section } from '@radix-ui/themes' import Link from 'next/link' -import { JobList } from './job-list' +import { JobList } from './components/list/job-list' export default async function Home() { // TODO: Fix user management @@ -13,25 +13,22 @@ export default async function Home() { return ( <>
- + Oh! What a tracker! - -
- {isAdmin(user) && ( -
- - - - - -
- )} -
- + {isAdmin(user) && ( + + + + + + )} +
+ + ) } diff --git a/bun.lockb b/bun.lockb index 31a9b154b67ebaa96bb07d3f17450b279fc769b7..e93ad8187b24b6f91356be7b9b31adf1e164caf7 100755 GIT binary patch delta 8097 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 MI+YyB7lm#91InNeZ2$lO delta 7853 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 statement-breakpoint +ALTER TABLE "jobs" ADD COLUMN "is_hidden" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "jobs" ADD COLUMN "is_top_choice" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "jobs" ADD COLUMN "is_applied" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..88e2776 --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,232 @@ +{ + "id": "3f6c5f5d-1c9c-46ed-8595-732cbc03d7fa", + "prevId": "1affdfae-cbeb-4867-b25b-9dacad9b76f2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracker_url": { + "name": "tracker_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracker_type": { + "name": "tracker_type", + "type": "tracker_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hiring_platform": { + "name": "hiring_platform", + "type": "hiring_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "companies_tracker_url_unique": { + "name": "companies_tracker_url_unique", + "nullsNotDistinct": false, + "columns": [ + "tracker_url" + ] + } + } + }, + "public.jobs": { + "name": "jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_updated_at": { + "name": "last_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "departments": { + "name": "departments", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "company_id": { + "name": "company_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_seen": { + "name": "is_seen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_hidden": { + "name": "is_hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_top_choice": { + "name": "is_top_choice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_applied": { + "name": "is_applied", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "jobs_company_id_companies_id_fk": { + "name": "jobs_company_id_companies_id_fk", + "tableFrom": "jobs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "jobs_url_unique": { + "name": "jobs_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + } + }, + "enums": { + "public.hiring_platform": { + "name": "hiring_platform", + "schema": "public", + "values": [ + "greenhouse" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "open", + "closed" + ] + }, + "public.tracker_type": { + "name": "tracker_type", + "schema": "public", + "values": [ + "hiring_platform" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index bcfd705..71b179d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1717929473056, "tag": "0006_marvelous_rumiko_fujikawa", "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1718042650768, + "tag": "0007_ancient_lady_bullseye", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/db/queries.ts b/lib/db/queries.ts index bf3b2a7..807b569 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -1,6 +1,6 @@ // TODO: split this file -import { and, eq, notInArray, sql } from 'drizzle-orm' +import { and, asc, eq, notInArray, sql } from 'drizzle-orm' import { db } from './db' import { type InsertCompany, type InsertJob, companies, jobs } from './schema' @@ -62,6 +62,7 @@ export const queryInsertJobs = async (jobList: InsertJob[]) => { status: 'open', }, }) + .returning({ id: jobs.id }) return result } @@ -73,6 +74,7 @@ export const queryGetJobs = async () => { with: { company: true, }, + orderBy: [asc(jobs.companyId), asc(jobs.title)], }) return result @@ -96,6 +98,7 @@ export const queryMarkJobsAsClosed = async ( ), ), ) + .returning({ id: jobs.id }) return result } diff --git a/lib/db/queries/job-action.ts b/lib/db/queries/job-action.ts new file mode 100644 index 0000000..91862ae --- /dev/null +++ b/lib/db/queries/job-action.ts @@ -0,0 +1,33 @@ +import { logger } from '@/lib/logger' +import { eq } from 'drizzle-orm' +import { db } from '../db' +import { type SelectJob, jobs } from '../schema' + +type JobActionField = keyof Pick< + SelectJob, + 'isTopChoice' | 'isHidden' | 'isSeen' +> + +const createQuery = + (field: JobActionField) => async (jobId: SelectJob['id'], value: boolean) => { + const l = logger.child({ jobId, field, value }) + + l.debug('Updating job') + + const result = await db + .update(jobs) + .set({ [field]: value }) + .where(eq(jobs.id, jobId)) + .returning({ id: jobs.id }) + + return result + } + +export const queryMarkTopChoice = createQuery('isTopChoice') +export type QueryMarkTopChoiceResult = ReturnType + +export const queryMarkHidden = createQuery('isHidden') +export type QueryMarkHiddenResult = ReturnType + +export const queryMarkSeen = createQuery('isSeen') +export type QueryMarkSeenResult = ReturnType diff --git a/lib/db/schema.ts b/lib/db/schema.ts index ccd959f..99d09f9 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -1,5 +1,6 @@ import { relations } from 'drizzle-orm' import { + boolean, integer, json, pgEnum, @@ -20,7 +21,10 @@ export const hiringPlatform = pgEnum('hiring_platform', hiringPlatforms) export const companies = pgTable('companies', { id: serial('id').primaryKey(), createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at') + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), name: text('name').notNull(), trackerURL: text('tracker_url').notNull().unique(), trackerType: trackerType('tracker_type').notNull(), @@ -41,7 +45,10 @@ export const jobStatus = pgEnum('job_status', jobStatuses) export const jobs = pgTable('jobs', { id: serial('id').primaryKey(), createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at') + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), url: text('url').notNull().unique(), title: text('title').notNull(), location: text('location').notNull(), @@ -52,6 +59,10 @@ export const jobs = pgTable('jobs', { companyId: integer('company_id') .notNull() .references(() => companies.id, { onDelete: 'cascade' }), + isSeen: boolean('is_seen').notNull().default(false), + isHidden: boolean('is_hidden').notNull().default(false), + isTopChoice: boolean('is_top_choice').notNull().default(false), + isApplied: boolean('is_applied').notNull().default(false), }) export type SelectJob = typeof jobs.$inferSelect diff --git a/package.json b/package.json index c57fb53..cde9bda 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "typecheck": "tsc --noEmit", "db:generate": "drizzle-kit generate --config drizzle.config.ts", "db:migrate": "bun run lib/db/migrate.ts | pino-pretty", - "db:reset": "bun run lib/db/reset.ts | pino-pretty" + "db:reset": "bun run lib/db/reset.ts | pino-pretty", + "prepare": "husky" }, "dependencies": { "@clerk/nextjs": "^5.1.4", @@ -40,6 +41,7 @@ "@types/react-dom": "^18", "dotenv": "^16.4.5", "drizzle-kit": "^0.22.1", + "husky": "^9.0.11", "typescript": "^5" } }