diff --git a/.env.example b/.env.example index 2293b74..15aad8f 100644 --- a/.env.example +++ b/.env.example @@ -34,7 +34,7 @@ NEXT_PUBLIC_MAPBOX_TOKEN=YOUR_CAMPUS_MAPBOX_TOKEN_HERE NEXT_PUBLIC_CAMPUS_MAP_TOKEN=YOUR_CAMPUS_MAP_TOKEN_HERE NEXT_PUBLIC_CAMPUS_MAP_STYLE=YOUR_CAMPUS_MAP_STYLE_HERE -# ----- InboxEngine (org/venue/event sync, apps/database) ----- +# ----- InboxEngine (org/venue/event sync in apps/database; org emails in apps/web) ----- # INBOX_ENGINE_URL=https://inbox-engine.tigerapps.org # INBOX_ENGINE_TOKEN=ask-a-tigerapps-admin diff --git a/apps/database/drizzle/0005_user_calendar_token.sql b/apps/database/drizzle/0005_user_calendar_token.sql new file mode 100644 index 0000000..6678ef0 --- /dev/null +++ b/apps/database/drizzle/0005_user_calendar_token.sql @@ -0,0 +1,2 @@ +ALTER TABLE "users" ADD COLUMN "calendar_token" varchar(64);--> statement-breakpoint +ALTER TABLE "users" ADD CONSTRAINT "users_calendar_token_unique" UNIQUE("calendar_token"); \ No newline at end of file diff --git a/apps/database/drizzle/meta/0005_snapshot.json b/apps/database/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..9a65fd8 --- /dev/null +++ b/apps/database/drizzle/meta/0005_snapshot.json @@ -0,0 +1,1931 @@ +{ + "id": "9345e621-b354-4574-95a7-965d40fd27c7", + "prevId": "5808d9c6-cd09-4f36-9ac1-b06891802056", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.campus_locations": { + "name": "campus_locations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(100)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "location_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event_tag_embeddings": { + "name": "event_tag_embeddings", + "schema": "", + "columns": { + "tag_name": { + "name": "tag_name", + "type": "event_tag", + "typeSchema": "public", + "primaryKey": true, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event_tags": { + "name": "event_tags", + "schema": "", + "columns": { + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "event_tag", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "event_tags_tag_idx": { + "name": "event_tags_tag_idx", + "columns": [ + { + "expression": "tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "event_tags_event_id_events_id_fk": { + "name": "event_tags_event_id_events_id_fk", + "tableFrom": "event_tags", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "event_tags_event_id_tag_pk": { + "name": "event_tags_event_id_tag_pk", + "columns": [ + "event_id", + "tag" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "datetime": { + "name": "datetime", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_datetime": { + "name": "end_datetime", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "flyer_url": { + "name": "flyer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_preset": { + "name": "cover_preset", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "external_link": { + "name": "external_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'published'" + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "source_message_id": { + "name": "source_message_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location_detail": { + "name": "location_detail", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": { + "events_published_datetime_idx": { + "name": "events_published_datetime_idx", + "columns": [ + { + "expression": "datetime", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"events\".\"status\" = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_org_id_idx": { + "name": "events_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_creator_id_idx": { + "name": "events_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_location_id_idx": { + "name": "events_location_id_idx", + "columns": [ + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_location_id_campus_locations_id_fk": { + "name": "events_location_id_campus_locations_id_fk", + "tableFrom": "events", + "tableTo": "campus_locations", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "events_org_id_organizations_id_fk": { + "name": "events_org_id_organizations_id_fk", + "tableFrom": "events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_creator_id_users_id_fk": { + "name": "events_creator_id_users_id_fk", + "tableFrom": "events", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "events_source_message_id_unique": { + "name": "events_source_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "source_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friendships": { + "name": "friendships", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "friend_id": { + "name": "friend_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "friendship_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "friendships_friend_id_status_idx": { + "name": "friendships_friend_id_status_idx", + "columns": [ + { + "expression": "friend_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friendships_user_id_users_id_fk": { + "name": "friendships_user_id_users_id_fk", + "tableFrom": "friendships", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "friendships_friend_id_users_id_fk": { + "name": "friendships_friend_id_users_id_fk", + "tableFrom": "friendships", + "tableTo": "users", + "columnsFrom": [ + "friend_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "friendships_user_id_friend_id_pk": { + "name": "friendships_user_id_friend_id_pk", + "columns": [ + "user_id", + "friend_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interactions": { + "name": "interactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "item_type": { + "name": "item_type", + "type": "item_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'event'" + }, + "interaction_type": { + "name": "interaction_type", + "type": "interaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "interaction_value": { + "name": "interaction_value", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interactions_user_id_created_at_idx": { + "name": "interactions_user_id_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interactions_item_id_type_idx": { + "name": "interactions_item_id_type_idx", + "columns": [ + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "interaction_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interactions_user_id_users_id_fk": { + "name": "interactions_user_id_users_id_fk", + "tableFrom": "interactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.listserv_configs": { + "name": "listserv_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gmail_label": { + "name": "gmail_label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "listserv_configs_org_id_organizations_id_fk": { + "name": "listserv_configs_org_id_organizations_id_fk", + "tableFrom": "listserv_configs", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "listserv_configs_address_unique": { + "name": "listserv_configs_address_unique", + "nullsNotDistinct": false, + "columns": [ + "address" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.listserv_emails": { + "name": "listserv_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "message_id": { + "name": "message_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "listserv": { + "name": "listserv", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "author_email": { + "name": "author_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_hoagiemail": { + "name": "is_hoagiemail", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hoagiemail_sender_name": { + "name": "hoagiemail_sender_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "hoagiemail_sender_email": { + "name": "hoagiemail_sender_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "attachments": { + "name": "attachments", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "listserv_url": { + "name": "listserv_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "listserv_emails_message_id_unique": { + "name": "listserv_emails_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_id_created_at_idx": { + "name": "notifications_user_id_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_followers": { + "name": "org_followers", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_followers_user_id_idx": { + "name": "org_followers_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_followers_org_id_organizations_id_fk": { + "name": "org_followers_org_id_organizations_id_fk", + "tableFrom": "org_followers", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_followers_user_id_users_id_fk": { + "name": "org_followers_user_id_users_id_fk", + "tableFrom": "org_followers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_followers_org_id_user_id_pk": { + "name": "org_followers_org_id_user_id_pk", + "columns": [ + "org_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_members": { + "name": "org_members", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "org_members_user_id_idx": { + "name": "org_members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_members_org_id_organizations_id_fk": { + "name": "org_members_org_id_organizations_id_fk", + "tableFrom": "org_members", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_members_user_id_users_id_fk": { + "name": "org_members_user_id_users_id_fk", + "tableFrom": "org_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_members_org_id_user_id_pk": { + "name": "org_members_org_id_user_id_pk", + "columns": [ + "org_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "org_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "external_id": { + "name": "external_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "acronym": { + "name": "acronym", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_type": { + "name": "group_type", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "group_url": { + "name": "group_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "socials": { + "name": "socials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "member_count": { + "name": "member_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": { + "organizations_creator_id_users_id_fk": { + "name": "organizations_creator_id_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_name_unique": { + "name": "organizations_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "organizations_external_id_unique": { + "name": "organizations_external_id_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_logs": { + "name": "pipeline_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "message_id": { + "name": "message_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "listserv_config_id": { + "name": "listserv_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "pipeline_log_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error_text": { + "name": "error_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extracted_event_id": { + "name": "extracted_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pipeline_logs_listserv_config_id_listserv_configs_id_fk": { + "name": "pipeline_logs_listserv_config_id_listserv_configs_id_fk", + "tableFrom": "pipeline_logs", + "tableTo": "listserv_configs", + "columnsFrom": [ + "listserv_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_logs_extracted_event_id_events_id_fk": { + "name": "pipeline_logs_extracted_event_id_events_id_fk", + "tableFrom": "pipeline_logs", + "tableTo": "events", + "columnsFrom": [ + "extracted_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rsvps": { + "name": "rsvps", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rsvps_event_id_idx": { + "name": "rsvps_event_id_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rsvps_user_id_users_id_fk": { + "name": "rsvps_user_id_users_id_fk", + "tableFrom": "rsvps", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rsvps_event_id_events_id_fk": { + "name": "rsvps_event_id_events_id_fk", + "tableFrom": "rsvps", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "rsvps_user_id_event_id_pk": { + "name": "rsvps_user_id_event_id_pk", + "columns": [ + "user_id", + "event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_events": { + "name": "saved_events", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "saved_events_event_id_idx": { + "name": "saved_events_event_id_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saved_events_user_id_users_id_fk": { + "name": "saved_events_user_id_users_id_fk", + "tableFrom": "saved_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saved_events_event_id_events_id_fk": { + "name": "saved_events_event_id_events_id_fk", + "tableFrom": "saved_events", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saved_events_user_id_event_id_pk": { + "name": "saved_events_user_id_event_id_pk", + "columns": [ + "user_id", + "event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_state": { + "name": "sync_state", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(100)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_interests": { + "name": "user_interests", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "event_tag", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_interests_user_id_users_id_fk": { + "name": "user_interests_user_id_users_id_fk", + "tableFrom": "user_interests", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_interests_user_id_tag_pk": { + "name": "user_interests_user_id_tag_pk", + "columns": [ + "user_id", + "tag" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preference_vectors": { + "name": "user_preference_vectors", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "tag_weights": { + "name": "tag_weights", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "latent_vector": { + "name": "latent_vector", + "type": "double precision[]", + "primaryKey": false, + "notNull": false + }, + "interaction_count": { + "name": "interaction_count", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preference_vectors_user_id_users_id_fk": { + "name": "user_preference_vectors_user_id_users_id_fk", + "tableFrom": "user_preference_vectors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_regions": { + "name": "user_regions", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "campus_region", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_regions_user_id_users_id_fk": { + "name": "user_regions_user_id_users_id_fk", + "tableFrom": "user_regions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_regions_user_id_region_pk": { + "name": "user_regions_user_id_region_pk", + "columns": [ + "user_id", + "region" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "net_id": { + "name": "net_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "class_year": { + "name": "class_year", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "major": { + "name": "major", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_org_leader": { + "name": "is_org_leader", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "onboarded": { + "name": "onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "calendar_token": { + "name": "calendar_token", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_net_id_unique": { + "name": "users_net_id_unique", + "nullsNotDistinct": false, + "columns": [ + "net_id" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_calendar_token_unique": { + "name": "users_calendar_token_unique", + "nullsNotDistinct": false, + "columns": [ + "calendar_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.campus_region": { + "name": "campus_region", + "schema": "public", + "values": [ + "central", + "east", + "west", + "south", + "north", + "off-campus" + ] + }, + "public.event_status": { + "name": "event_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.event_tag": { + "name": "event_tag", + "schema": "public", + "values": [ + "free food", + "career", + "research", + "academics", + "tech", + "entrepreneurship", + "politics", + "visual arts", + "performing arts", + "literature", + "culture", + "music", + "gaming", + "athletics", + "religion", + "sustainability", + "outdoors", + "wellness", + "community service", + "speaker event", + "social event", + "stem" + ] + }, + "public.friendship_status": { + "name": "friendship_status", + "schema": "public", + "values": [ + "pending", + "accepted", + "declined" + ] + }, + "public.interaction_type": { + "name": "interaction_type", + "schema": "public", + "values": [ + "view", + "click", + "rsvp", + "save", + "share", + "hide" + ] + }, + "public.item_type": { + "name": "item_type", + "schema": "public", + "values": [ + "event", + "organization" + ] + }, + "public.location_category": { + "name": "location_category", + "schema": "public", + "values": [ + "academic", + "residential", + "athletic", + "social", + "administrative", + "library", + "dining", + "other" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "friend_request", + "event_reminder", + "org_new_event" + ] + }, + "public.org_category": { + "name": "org_category", + "schema": "public", + "values": [ + "career", + "affinity", + "performing arts", + "academics", + "athletics", + "social event", + "culture", + "religion", + "politics", + "community service" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": [ + "owner", + "officer", + "member" + ] + }, + "public.pipeline_log_status": { + "name": "pipeline_log_status", + "schema": "public", + "values": [ + "success", + "skipped_not_event", + "duplicate", + "error", + "needs_review" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/database/drizzle/meta/_journal.json b/apps/database/drizzle/meta/_journal.json index 654b258..af25693 100644 --- a/apps/database/drizzle/meta/_journal.json +++ b/apps/database/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1790579109504, "tag": "0004_inbox_engine_orgs_events", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1790583252834, + "tag": "0005_user_calendar_token", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/database/src/schema/index.ts b/apps/database/src/schema/index.ts index d399341..69ab2fd 100644 --- a/apps/database/src/schema/index.ts +++ b/apps/database/src/schema/index.ts @@ -114,6 +114,8 @@ export const users = pgTable("users", { avatarUrl: text("avatar_url"), isOrgLeader: boolean("is_org_leader").default(false).notNull(), onboarded: boolean("onboarded").default(false).notNull(), + /** Secret for the user's iCalendar feed URLs (/cal//…). Created on first use; rotatable. */ + calendarToken: varchar("calendar_token", { length: 64 }).unique(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); diff --git a/apps/web/.env.local.example b/apps/web/.env.local.example index ce63720..e809c30 100644 --- a/apps/web/.env.local.example +++ b/apps/web/.env.local.example @@ -28,3 +28,7 @@ NEXT_PUBLIC_CAMPUS_MAP_STYLE=YOUR_CAMPUS_MAP_STYLE_HERE # AWS S3 (image uploads) # AWS_S3_BUCKET=the-forum-uploads # AWS_REGION=us-east-1 + +# InboxEngine API — powers "Recent emails" on org pages (optional) +# INBOX_ENGINE_URL=http://127.0.0.1:8300 +# INBOX_ENGINE_TOKEN=local-forum-token-0123456789abcdef diff --git a/apps/web/src/actions/calendar.ts b/apps/web/src/actions/calendar.ts new file mode 100644 index 0000000..fe4d6d6 --- /dev/null +++ b/apps/web/src/actions/calendar.ts @@ -0,0 +1,149 @@ +"use server"; + +import { headers } from "next/headers"; +import { auth } from "~/auth"; +import { + type FeedEventRow, + ensureCalendarToken, + eventsForIds, + followedOrgs, + orgEvents, + rotateCalendarToken, + rsvpedEventIds, + savedEventIds, +} from "~/lib/calendar-feeds"; +import { getAppOrigin } from "~/lib/cas-server"; + +export type CalendarId = "going" | "saved" | `org-${string}`; + +export interface CalendarInfo { + id: CalendarId; + name: string; + kind: "going" | "saved" | "org"; + orgId?: string; + logoUrl?: string | null; + /** Org calendars: true if followed; false = suggested from your RSVPs/saves. */ + followed?: boolean; + /** Secret feed URL (https) for subscribing/downloading. */ + feedUrl: string; +} + +export interface CalendarEvent { + id: string; + title: string; + start: string; + end: string | null; + location: string | null; + locationDetail: string | null; + orgName: string | null; + /** Every calendar this event belongs to (an RSVP'd event from a followed org is in both). */ + calendars: CalendarId[]; +} + +export interface MyCalendars { + calendars: CalendarInfo[]; + /** Combined "All my Forum events" feed. */ + allFeedUrl: string; + events: CalendarEvent[]; +} + +async function requireUserId() { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + return session.user.id; +} + +async function feedBase(token: string) { + const origin = getAppOrigin(await headers()); + return `${origin}/cal/${token}`; +} + +/** + * Everything the My Events calendar needs: the user's calendars (with secret + * feed URLs — the token is created on first use) and the events in them from + * two months back onwards. + */ +export async function getMyCalendars(): Promise { + const userId = await requireUserId(); + const token = await ensureCalendarToken(userId); + const base = await feedBase(token); + + const [goingIds, savedIds, followed] = await Promise.all([ + rsvpedEventIds(userId), + savedEventIds(userId), + followedOrgs(userId), + ]); + const mine = await eventsForIds(userId, [...new Set([...goingIds, ...savedIds])]); + + // Orgs behind your RSVPs/saves that you don't follow are offered too. + const followedIds = new Set(followed.map((o) => o.id)); + const suggested = new Map(); + for (const e of mine) { + if (e.orgId && e.orgName && !followedIds.has(e.orgId)) suggested.set(e.orgId, e.orgName); + } + const orgIds = [...followedIds, ...suggested.keys()]; + const fromOrgs = await orgEvents(userId, orgIds); + + const going = new Set(goingIds); + const saved = new Set(savedIds); + const byId = new Map }>(); + const add = (row: FeedEventRow, cal: CalendarId) => { + const entry = byId.get(row.id) ?? { row, calendars: new Set() }; + entry.calendars.add(cal); + byId.set(row.id, entry); + }; + for (const row of mine) { + if (going.has(row.id)) add(row, "going"); + if (saved.has(row.id)) add(row, "saved"); + } + for (const row of fromOrgs) if (row.orgId) add(row, `org-${row.orgId}`); + + const calendars: CalendarInfo[] = [ + { id: "going", name: "Going", kind: "going", feedUrl: `${base}/going.ics` }, + { id: "saved", name: "Saved", kind: "saved", feedUrl: `${base}/saved.ics` }, + ...followed.map( + (o): CalendarInfo => ({ + id: `org-${o.id}`, + name: o.name, + kind: "org", + orgId: o.id, + logoUrl: o.logoUrl, + followed: true, + feedUrl: `${base}/org-${o.id}.ics`, + }), + ), + ...[...suggested.entries()] + .sort((a, b) => a[1].localeCompare(b[1])) + .map( + ([id, name]): CalendarInfo => ({ + id: `org-${id}`, + name, + kind: "org", + orgId: id, + followed: false, + feedUrl: `${base}/org-${id}.ics`, + }), + ), + ]; + + const events: CalendarEvent[] = [...byId.values()] + .sort((a, b) => a.row.datetime.getTime() - b.row.datetime.getTime()) + .map(({ row, calendars: cals }) => ({ + id: row.id, + title: row.title, + start: row.datetime.toISOString(), + end: row.endDatetime?.toISOString() ?? null, + location: row.locationName, + locationDetail: row.locationDetail, + orgName: row.orgName, + calendars: [...cals], + })); + + return { calendars, allFeedUrl: `${base}/all.ics`, events }; +} + +/** Rotate the secret: every previously shared feed URL stops working. */ +export async function resetCalendarLink(): Promise { + const userId = await requireUserId(); + await rotateCalendarToken(userId); +} diff --git a/apps/web/src/actions/inbox.ts b/apps/web/src/actions/inbox.ts new file mode 100644 index 0000000..290f9a1 --- /dev/null +++ b/apps/web/src/actions/inbox.ts @@ -0,0 +1,18 @@ +"use server"; + +import { auth } from "~/auth"; +import { type InboxMessageDetail, getEmailById } from "~/lib/inbox-engine"; +import { checkRateLimit } from "~/lib/rate-limit"; + +/** + * Loads one full email for the org-page reader. Signed-in users only + * (rate-limited); returns null — never throws — when the email can't be + * fetched, so the reader can show a friendly fallback. + */ +export async function getInboxEmail(id: string): Promise { + const session = await auth(); + if (!session?.user?.id) return null; + if (typeof id !== "string") return null; + if (!checkRateLimit("readEmail", session.user.id).ok) return null; + return getEmailById(id); +} diff --git a/apps/web/src/actions/orgs.ts b/apps/web/src/actions/orgs.ts index 7fbf64b..e5b7396 100644 --- a/apps/web/src/actions/orgs.ts +++ b/apps/web/src/actions/orgs.ts @@ -57,6 +57,8 @@ export interface OrgDetail { acronym: string | null; tagline: string | null; groupType: string | null; + /** MyPrincetonU id ("mpu:70043") for imported groups — keys their emails in InboxEngine. */ + externalId: string | null; /** The group's MyPrincetonU page. */ groupUrl: string | null; website: string | null; @@ -262,6 +264,7 @@ export async function getOrg(orgId: string): Promise { acronym: org.acronym, tagline: org.tagline, groupType: org.groupType, + externalId: org.externalId, groupUrl: org.groupUrl, website: org.website, contactEmail: org.contactEmail, diff --git a/apps/web/src/app/(app)/events/[id]/event-detail-client.tsx b/apps/web/src/app/(app)/events/[id]/event-detail-client.tsx index 1b3abae..302fe00 100644 --- a/apps/web/src/app/(app)/events/[id]/event-detail-client.tsx +++ b/apps/web/src/app/(app)/events/[id]/event-detail-client.tsx @@ -26,6 +26,7 @@ import { toggleSave, } from "~/actions/events"; import { OrgAvatar } from "~/components/common/org-avatar"; +import { RichText } from "~/components/common/rich-text"; import { AttendeesDialog } from "~/components/events/attendees-dialog"; import { EventCoverArt } from "~/components/events/event-cover-art"; import { MiniEventList } from "~/components/events/mini-event-list"; @@ -288,9 +289,7 @@ export function EventDetailClient({ event, similarEvents }: EventDetailClientPro )} -

- {event.description} -

+
{event.source === "myprincetonu" || event.source === "listserv" ? ( diff --git a/apps/web/src/app/(app)/events/my-events-client.tsx b/apps/web/src/app/(app)/events/my-events-client.tsx index 50cffbc..0f686a6 100644 --- a/apps/web/src/app/(app)/events/my-events-client.tsx +++ b/apps/web/src/app/(app)/events/my-events-client.tsx @@ -3,6 +3,7 @@ import { useState, useTransition } from "react"; import { toast } from "sonner"; import { type FeedEvent, deleteEvent, toggleRsvp, toggleSave } from "~/actions/events"; +import { CalendarView } from "~/components/calendar/calendar-view"; import { EmptyState } from "~/components/common/states"; import { EventCard } from "~/components/events/event-card"; import { EventCollection, EventViewToggle } from "~/components/events/event-collection"; @@ -119,56 +120,70 @@ export function MyEventsClient({ created, rsvped, saved }: MyEventsClientProps) return ( setActiveTab(value as TabId)}>
- - {TABS.map(({ id, label }) => ( - - {label} - {eventMap[id].length} - - ))} - - + {view === "calendar" ? ( +

+ Going, Saved and the organizations you follow +

+ ) : ( + + {TABS.map(({ id, label }) => ( + + {label} + + {eventMap[id].length} + + + ))} + + )} +
- {TABS.map(({ id, emptyTitle, emptyBody }) => ( - - {eventMap[id].length > 0 ? ( - ( - setPendingDelete(event), - } - : {})} - onSaveToggle={() => handleSaveToggle(event.id)} - onRsvpToggle={() => handleRsvpToggle(event.id)} - onShare={() => { - navigator.clipboard.writeText(`${window.location.origin}/events/${event.id}`); - toast.success("Link copied to clipboard"); - }} - source="feed" - position={index} - /> - )} - /> - ) : ( - - )} - - ))} + {view === "calendar" ? ( +
+ +
+ ) : ( + TABS.map(({ id, emptyTitle, emptyBody }) => ( + + {eventMap[id].length > 0 ? ( + ( + setPendingDelete(event), + } + : {})} + onSaveToggle={() => handleSaveToggle(event.id)} + onRsvpToggle={() => handleRsvpToggle(event.id)} + onShare={() => { + navigator.clipboard.writeText(`${window.location.origin}/events/${event.id}`); + toast.success("Link copied to clipboard"); + }} + source="feed" + position={index} + /> + )} + /> + ) : ( + + )} + + )) + )} !open && setPendingDelete(null)}> diff --git a/apps/web/src/app/(app)/orgs/[id]/org-profile-client.tsx b/apps/web/src/app/(app)/orgs/[id]/org-profile-client.tsx index 3e07499..860f9a1 100644 --- a/apps/web/src/app/(app)/orgs/[id]/org-profile-client.tsx +++ b/apps/web/src/app/(app)/orgs/[id]/org-profile-client.tsx @@ -19,11 +19,14 @@ import { toast } from "sonner"; import { type UserSearchResult, searchUsers } from "~/actions/friends"; import { type OrgDetail, addOfficer, removeOfficer, toggleFollowOrg } from "~/actions/orgs"; import { OrgAvatar } from "~/components/common/org-avatar"; +import { RichText } from "~/components/common/rich-text"; import { SearchInput } from "~/components/common/search-input"; import { EmptyState } from "~/components/common/states"; import { EventCard } from "~/components/events/event-card"; import { PageShell } from "~/components/layout/page-shell"; +import { OrgEmails } from "~/components/orgs/org-emails"; import { Button } from "~/components/ui/button"; +import type { OrgEmails as OrgEmailsData } from "~/lib/inbox-engine"; const SOCIAL_LABELS: Record = { instagram: "Instagram", @@ -70,7 +73,16 @@ function SideSection({ title, children }: { title: string; children: React.React ); } -export function OrgProfileClient({ org }: { org: OrgDetail }) { +export function OrgProfileClient({ + org, + emails, + emailsBrowseUrl, +}: { + org: OrgDetail; + /** Recent listserv emails from InboxEngine (MyPrincetonU groups only). */ + emails: OrgEmailsData | null; + emailsBrowseUrl: string | null; +}) { const router = useRouter(); const [isPending, startTransition] = useTransition(); const [isFollowing, setIsFollowing] = useState(org.isFollowing); @@ -146,6 +158,67 @@ export function OrgProfileClient({ org }: { org: OrgDetail }) { } }; + const hasEmails = Boolean(emails && emails.messages.length > 0 && emailsBrowseUrl); + // No upcoming events? Then the org's emails are the most useful thing here. + const emailsFirst = hasEmails && org.upcomingEvents.length === 0; + const emailsSection = + hasEmails && emails && emailsBrowseUrl ? ( + + ) : null; + const eventsSection = ( +
+

+ Upcoming events + {org.upcomingEvents.length} +

+ {org.upcomingEvents.length > 0 ? ( +
+ {org.upcomingEvents.map((event, index) => ( + + ))} +
+ ) : hasEmails ? ( +

+ No upcoming events right now — their recent emails are above. +

+ ) : ( + + )} +
+ ); + const socials = Object.entries(org.socials).filter(([k, v]) => SOCIAL_LABELS[k] && v); return ( @@ -231,54 +304,21 @@ export function OrgProfileClient({ org }: { org: OrgDetail }) { {org.description && (

About

-

- {org.description} -

+
)} -
-

- Upcoming events - {org.upcomingEvents.length} -

- {org.upcomingEvents.length > 0 ? ( -
- {org.upcomingEvents.map((event, index) => ( - - ))} -
- ) : ( - - )} -
+ {emailsFirst ? ( + <> + {emailsSection} + {eventsSection} + + ) : ( + <> + {eventsSection} + {emailsSection} + + )}