diff --git a/drizzle/0017_bang_counter.sql b/drizzle/0017_bang_counter.sql new file mode 100644 index 0000000..5a50206 --- /dev/null +++ b/drizzle/0017_bang_counter.sql @@ -0,0 +1,13 @@ +CREATE TABLE "bang_events" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "household_id" uuid NOT NULL, + "recorded_by" uuid, + "occurred_on" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "bang_events" ADD CONSTRAINT "bang_events_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "bang_events" ADD CONSTRAINT "bang_events_recorded_by_users_id_fk" FOREIGN KEY ("recorded_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action; +--> statement-breakpoint +CREATE INDEX "bang_events_household_occurred_idx" ON "bang_events" USING btree ("household_id","occurred_on"); diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json index a4e2186..7e4b296 100644 --- a/drizzle/meta/0008_snapshot.json +++ b/drizzle/meta/0008_snapshot.json @@ -1,6 +1,6 @@ { - "id": "fc208bad-8bfd-452d-876f-7e853f699931", - "prevId": "cb06cd57-d5a0-4fb7-bbf3-335173a1168c", + "id": "ff1f5ff1-7539-4c5e-a20d-f01c719ef198", + "prevId": "fc208bad-8bfd-452d-876f-7e853f699931", "version": "7", "dialect": "postgresql", "tables": { diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..737e7ac --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,1397 @@ +{ + "id": "a7c89adf-0d22-49a9-81cb-bbf3d4472251", + "prevId": "ff1f5ff1-7539-4c5e-a20d-f01c719ef198", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "accounts_provider_provider_account_id_pk": { + "name": "accounts_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_household_created_idx": { + "name": "activity_log_household_created_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_household_id_households_id_fk": { + "name": "activity_log_household_id_households_id_fk", + "tableFrom": "activity_log", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "activity_log_actor_id_users_id_fk": { + "name": "activity_log_actor_id_users_id_fk", + "tableFrom": "activity_log", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.household_members": { + "name": "household_members", + "schema": "", + "columns": { + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "household_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": {}, + "foreignKeys": { + "household_members_household_id_households_id_fk": { + "name": "household_members_household_id_households_id_fk", + "tableFrom": "household_members", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "household_members_user_id_users_id_fk": { + "name": "household_members_user_id_users_id_fk", + "tableFrom": "household_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "household_members_household_id_user_id_pk": { + "name": "household_members_household_id_user_id_pk", + "columns": [ + "household_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.households": { + "name": "households", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminders": { + "name": "reminders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "fire_at": { + "name": "fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'in_app'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reminders_entity_unique": { + "name": "reminders_entity_unique", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reminders_household_fire_at_idx": { + "name": "reminders_household_fire_at_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reminders_household_id_households_id_fk": { + "name": "reminders_household_id_households_id_fk", + "tableFrom": "reminders", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "theme_mode": { + "name": "theme_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "default_dashboard_layout": { + "name": "default_dashboard_layout", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification_tokens": { + "name": "verification_tokens", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_tokens_identifier_token_pk": { + "name": "verification_tokens_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.calendar_events": { + "name": "calendar_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "calendar_id": { + "name": "calendar_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "all_day": { + "name": "all_day", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_source": { + "name": "external_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "calendar_events_calendar_start_idx": { + "name": "calendar_events_calendar_start_idx", + "columns": [ + { + "expression": "calendar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "calendar_events_calendar_id_calendars_id_fk": { + "name": "calendar_events_calendar_id_calendars_id_fk", + "tableFrom": "calendar_events", + "tableTo": "calendars", + "columnsFrom": [ + "calendar_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendar_events_owner_id_users_id_fk": { + "name": "calendar_events_owner_id_users_id_fk", + "tableFrom": "calendar_events", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "calendar_events_range_check": { + "name": "calendar_events_range_check", + "value": "\"calendar_events\".\"end_at\" >= \"calendar_events\".\"start_at\"" + } + }, + "isRLSEnabled": false + }, + "public.calendars": { + "name": "calendars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'household'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "calendars_household_idx": { + "name": "calendars_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendars_owner_idx": { + "name": "calendars_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "calendars_household_id_households_id_fk": { + "name": "calendars_household_id_households_id_fk", + "tableFrom": "calendars", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "calendars_owner_id_users_id_fk": { + "name": "calendars_owner_id_users_id_fk", + "tableFrom": "calendars", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "calendars_visibility_check": { + "name": "calendars_visibility_check", + "value": "\"calendars\".\"visibility\" in ('private', 'household')" + } + }, + "isRLSEnabled": false + }, + "public.list_items": { + "name": "list_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "list_id": { + "name": "list_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "done": { + "name": "done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "qty": { + "name": "qty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "list_items_list_position_idx": { + "name": "list_items_list_position_idx", + "columns": [ + { + "expression": "list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "list_items_assignee_idx": { + "name": "list_items_assignee_idx", + "columns": [ + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "list_items_list_id_lists_id_fk": { + "name": "list_items_list_id_lists_id_fk", + "tableFrom": "list_items", + "tableTo": "lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_items_assignee_id_users_id_fk": { + "name": "list_items_assignee_id_users_id_fk", + "tableFrom": "list_items", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lists": { + "name": "lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "lists_household_type_idx": { + "name": "lists_household_type_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lists_household_archived_idx": { + "name": "lists_household_archived_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lists_household_id_households_id_fk": { + "name": "lists_household_id_households_id_fk", + "tableFrom": "lists", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notes": { + "name": "notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remind_at": { + "name": "remind_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notes_household_pinned_updated_idx": { + "name": "notes_household_pinned_updated_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pinned", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notes_author_idx": { + "name": "notes_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notes_household_id_households_id_fk": { + "name": "notes_household_id_households_id_fk", + "tableFrom": "notes", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notes_author_id_users_id_fk": { + "name": "notes_author_id_users_id_fk", + "tableFrom": "notes", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bang_events": { + "name": "bang_events", + "schema": "public", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recorded_by": { + "name": "recorded_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "occurred_on": { + "name": "occurred_on", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bang_events_household_occurred_idx": { + "name": "bang_events_household_occurred_idx", + "columns": [ + { + "expression": "household_id", + "asc": true, + "nulls": "last", + "isExpression": false + }, + { + "expression": "occurred_on", + "asc": true, + "nulls": "last", + "isExpression": false + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree" + } + }, + "foreignKeys": { + "bang_events_household_id_households_id_fk": { + "name": "bang_events_household_id_households_id_fk", + "tableFrom": "bang_events", + "tableTo": "households", + "schemaTo": "public", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bang_events_recorded_by_users_id_fk": { + "name": "bang_events_recorded_by_users_id_fk", + "tableFrom": "bang_events", + "tableTo": "users", + "schemaTo": "public", + "columnsFrom": [ + "recorded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.household_member_role": { + "name": "household_member_role", + "schema": "public", + "values": [ + "owner", + "member" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 33f83e7..51e5ee4 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1748822400000, "tag": "0016_garden_improvements", "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1780391596552, + "tag": "0017_bang_counter", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 634698c..6c0c1a1 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@playwright/test": "^1.59.1", "@release-it/conventional-changelog": "^11.0.1", "@tailwindcss/postcss": "^4.2.4", + "@types/canvas-confetti": "^1.9.0", "@types/node": "^22.9.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -79,6 +80,7 @@ "@fullcalendar/interaction": "^6.1.20", "@fullcalendar/react": "^6.1.20", "@fullcalendar/timegrid": "^6.1.20", + "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9da1b7d..f8e67f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@fullcalendar/timegrid': specifier: ^6.1.20 version: 6.1.20(@fullcalendar/core@6.1.20) + canvas-confetti: + specifier: ^1.9.4 + version: 1.9.4 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -102,6 +105,9 @@ importers: '@tailwindcss/postcss': specifier: ^4.2.4 version: 4.2.4 + '@types/canvas-confetti': + specifier: ^1.9.0 + version: 1.9.0 '@types/node': specifier: ^22.9.0 version: 22.19.17 @@ -1844,6 +1850,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/canvas-confetti@1.9.0': + resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2280,6 +2289,9 @@ packages: caniuse-lite@1.0.30001791: resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + canvas-confetti@1.9.4: + resolution: {integrity: sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -6531,6 +6543,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/canvas-confetti@1.9.0': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -6977,6 +6991,8 @@ snapshots: caniuse-lite@1.0.30001791: {} + canvas-confetti@1.9.4: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 diff --git a/src/app/globals.css b/src/app/globals.css index b536e30..2a8fa90 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1346,3 +1346,81 @@ select { inset 0 1px 2px rgba(0, 0, 0, 0.12), 0 0 0 2px rgba(244, 239, 229, 0.08); } + +/* ─── Bang Counter celebration animations ─────────────────────────────────── */ +@keyframes shake { + 0%, + 100% { + transform: translateX(0); + } + 10% { + transform: translateX(-6px) rotate(-1deg); + } + 20% { + transform: translateX(6px) rotate(1deg); + } + 30% { + transform: translateX(-5px) rotate(-0.8deg); + } + 40% { + transform: translateX(5px) rotate(0.8deg); + } + 50% { + transform: translateX(-4px) rotate(-0.5deg); + } + 60% { + transform: translateX(4px) rotate(0.5deg); + } + 70% { + transform: translateX(-2px); + } + 80% { + transform: translateX(2px); + } + 90% { + transform: translateX(-1px); + } +} + +@keyframes countPop { + 0% { + transform: scale(1); + } + 30% { + transform: scale(1.45); + } + 60% { + transform: scale(0.9); + } + 80% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} + +@keyframes emojiFloat { + 0% { + transform: translateY(0) scale(1); + opacity: 1; + } + 50% { + transform: translateY(-60px) scale(1.2); + opacity: 0.9; + } + 100% { + transform: translateY(-120px) scale(0.7); + opacity: 0; + } +} + +.animate-shake { + animation: shake 0.35s ease-in-out both; +} +.animate-count-pop { + animation: countPop 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; +} +.animate-emoji-float { + animation: emojiFloat 1.8s ease-out both; +} diff --git a/src/modules/bangs/components/bang-widget.tsx b/src/modules/bangs/components/bang-widget.tsx new file mode 100644 index 0000000..dadd032 --- /dev/null +++ b/src/modules/bangs/components/bang-widget.tsx @@ -0,0 +1,332 @@ +"use client"; + +import { useState, useTransition, useRef, useCallback } from "react"; +import { createPortal } from "react-dom"; +import confetti from "canvas-confetti"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import type { BangStatsDto } from "../server/queries"; +import { addBang } from "../server/actions"; + +// ─── Celebration engine ─────────────────────────────────────────────────────── + +function launchFirework(originX: number, originY: number) { + confetti({ + particleCount: 80, + startVelocity: 45, + spread: 360, + origin: { x: originX, y: originY }, + colors: ["#ff0", "#f0f", "#0ff", "#f60", "#0f6", "#06f"], + shapes: ["circle", "square"], + gravity: 0.8, + scalar: 1.2, + ticks: 200, + }); +} + +function launchSideCannon(side: "left" | "right") { + confetti({ + particleCount: 120, + angle: side === "left" ? 60 : 120, + spread: 55, + origin: { x: side === "left" ? 0 : 1, y: 0.65 }, + colors: ["#ff4e50", "#fc913a", "#f9d423", "#ede574", "#e1f5c4"], + shapes: ["circle", "square", "star"], + scalar: 1.1, + ticks: 300, + }); +} + +function launchStarBurst() { + const defaults = { + spread: 360, + ticks: 100, + gravity: 0, + decay: 0.94, + startVelocity: 30, + shapes: ["star"] as confetti.Shape[], + colors: ["FFE400", "FFBD00", "E89400", "FFCA6C", "FDFFB8"], + }; + function shoot() { + confetti({ ...defaults, particleCount: 40, scalar: 1.2, shapes: ["star"] }); + confetti({ ...defaults, particleCount: 15, scalar: 0.75, shapes: ["circle"] }); + } + shoot(); + setTimeout(shoot, 100); + setTimeout(shoot, 200); +} + +function synthFireworkSound() { + try { + const ctx = new AudioContext(); + // Rising "pew" tone + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.connect(gain); + gain.connect(ctx.destination); + osc.frequency.setValueAtTime(200, ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(900, ctx.currentTime + 0.12); + gain.gain.setValueAtTime(0.35, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.18); + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + 0.18); + + // Noise "boom" + const bufferSize = ctx.sampleRate * 0.4; + const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); + const data = buffer.getChannelData(0); + for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; + const noise = ctx.createBufferSource(); + noise.buffer = buffer; + const noiseGain = ctx.createGain(); + const bpFilter = ctx.createBiquadFilter(); + bpFilter.type = "bandpass"; + bpFilter.frequency.value = 150; + noise.connect(bpFilter); + bpFilter.connect(noiseGain); + noiseGain.connect(ctx.destination); + noiseGain.gain.setValueAtTime(0.6, ctx.currentTime + 0.15); + noiseGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.6); + noise.start(ctx.currentTime + 0.15); + noise.stop(ctx.currentTime + 0.6); + } catch { + // Autoplay policy blocked — silently skip + } +} + +type EmojiParticle = { + id: number; + emoji: string; + x: number; + y: number; + size: number; + delay: number; +}; + +const CELEBRATION_EMOJIS = [ + "💥", + "🎉", + "🔥", + "✨", + "🎊", + "🥳", + "💦", + "😈", + "🎆", + "⭐", + "🍆", + "💫", + "🌟", + "🎇", + "🍒", +]; + +function EmojiOverlay({ particles }: { particles: EmojiParticle[] }) { + if (particles.length === 0) return null; + return createPortal( +
+ Last {Math.min(stats.recent.length, maxRecentBangs)} +
+ {stats.recent.map((bang) => ( +No bangs yet. Add the first one!
+ )} +