From f9e08f420e75ce61790e6228cb48f7a9159bf94c Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Tue, 13 Oct 2026 19:59:41 -0400 Subject: [PATCH 1/6] WEB-169 add prettier dependency and specifications --- .prettierignore | 18 ++++++++++++++++++ .prettierrc | 7 +++++++ package-lock.json | 17 +++++++++++++++++ package.json | 1 + 4 files changed, 43 insertions(+) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..464f2da7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,18 @@ +# dependencies +node_modules + +# build +.next +out +build + +# testing +coverage + +# misc +supabase +prisma/migrations +package-lock.json + +src/generated +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..4a1222f5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/package-lock.json b/package-lock.json index 1110044d..54f5f93d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "eslint-config-custom": "*", "husky": "^9.1.7", "postcss": "^8.5.6", + "prettier": "^3.9.10", "prisma": "^7.5.0", "shadcn": "^4.1.0", "supabase": "^2.81.3", @@ -11021,6 +11022,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", diff --git a/package.json b/package.json index 08ad33e8..13e307f8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "eslint-config-custom": "*", "husky": "^9.1.7", "postcss": "^8.5.6", + "prettier": "^3.9.10", "prisma": "^7.5.0", "shadcn": "^4.1.0", "supabase": "^2.81.3", From a943ba4f855938b149867c65a7751b80b1eb63ed Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Tue, 13 Oct 2026 20:02:45 -0400 Subject: [PATCH 2/6] WEB-169 prettier initial run --- .prettierrc | 5 +- README.md | 4 +- database.json | 1588 ++++++++--------- docs/NEON-MIGRATION-INVESTIGATION.md | 34 +- docs/STYLING.md | 69 +- docs/SUPABASE_SETUP.md | 14 +- prisma.config.ts | 16 +- src/app/[[...puckPath]]/page.tsx | 6 +- src/app/editor/DocumentList.tsx | 44 +- src/app/editor/MediaLibrary.tsx | 52 +- src/app/editor/ResourceCard.tsx | 20 +- src/app/editor/RouteTable.tsx | 24 +- .../editor/[id]/[slug]/ActionBarOverride.tsx | 19 +- src/app/editor/[id]/[slug]/EditorPage.tsx | 2 +- .../editor/[id]/[slug]/VersionListPanel.tsx | 41 +- .../[id]/[slug]/VersionPluginContainer.tsx | 14 +- src/app/editor/[id]/[slug]/client.tsx | 33 +- .../[id]/[slug]/editor-save-context.tsx | 18 +- src/app/editor/[id]/[slug]/params.tsx | 9 +- src/app/editor/page.tsx | 1 - src/app/layout.tsx | 12 +- src/app/styles.css | 7 +- src/components/puck/blocks/button.tsx | 52 +- src/components/puck/blocks/columns.tsx | 9 +- src/components/puck/blocks/container.tsx | 44 +- src/components/puck/blocks/grid.tsx | 9 +- src/components/puck/blocks/media.tsx | 33 +- src/components/puck/blocks/rich-text.tsx | 33 +- src/components/puck/blocks/section.tsx | 29 +- .../puck/fields/media-field-client.tsx | 20 +- src/components/puck/fields/media-field.tsx | 7 +- .../puck/fields/responsive/client.tsx | 16 +- .../puck/fields/responsive/controls.test.ts | 22 +- .../puck/fields/responsive/index.tsx | 9 +- .../puck/fields/responsive/kinds.ts | 4 +- .../puck/fields/responsive/number-control.tsx | 3 +- .../puck/fields/responsive/select-control.tsx | 4 +- .../puck/fields/responsive/select-field.tsx | 5 +- src/components/puck/media-context.tsx | 24 +- src/components/ui/alert-dialog.tsx | 87 +- src/components/ui/button.tsx | 16 +- src/components/ui/card.tsx | 41 +- src/components/ui/collapsible.tsx | 16 +- src/components/ui/dialog-provider.tsx | 76 +- src/components/ui/dialog.tsx | 82 +- src/components/ui/input.tsx | 12 +- src/components/ui/sonner.tsx | 38 +- src/components/ui/use-dialog-promise.tsx | 2 +- src/hooks/useKeybind.ts | 9 +- src/lib/documents/actions.ts | 29 +- src/lib/documents/version-selection.ts | 5 +- src/lib/media/actions.ts | 20 +- src/lib/media/queries.ts | 1 - src/lib/prisma.ts | 8 +- src/lib/puck/define-props.ts | 36 +- src/lib/puck/layout.ts | 7 +- src/lib/puck/responsive-tailwind.test.ts | 25 +- src/lib/puck/responsive.test.ts | 4 +- src/lib/puck/tokens.test.ts | 8 +- src/lib/puck/tokens.ts | 219 +-- src/lib/routes/actions.ts | 17 +- src/lib/routes/utils.test.ts | 17 +- src/lib/routes/utils.ts | 2 +- src/lib/supabase.ts | 8 +- src/lib/types.ts | 4 +- src/lib/utils.ts | 10 +- src/puck.config.tsx | 5 +- tsconfig.json | 8 +- 68 files changed, 1545 insertions(+), 1622 deletions(-) diff --git a/.prettierrc b/.prettierrc index 4a1222f5..5e4c3e81 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,8 @@ { "semi": true, "singleQuote": false, + "tabWidth": 2, + "useTabs": false, "trailingComma": "all", - "printWidth": 100, - "tabWidth": 2 + "printWidth": 100 } diff --git a/README.md b/README.md index a595f1af..eca7c139 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SGA Website Internal CMS Application +# SGA Website Internal CMS Application This is SGA's internal application tool to allow for SGA's Webmaster Team to easily manage and update SGA's website. We are developing new features for administrators to modify the application without the need for changes in the core code structure, allowing for quick content management and visual edits throughout the website. @@ -29,7 +29,7 @@ This is SGA's internal application tool to allow for SGA's Webmaster Team to eas ### Cloning and Running the Project -We *highly* recommend the use of VSCode for this project. Please download the VSCode and clone the GitHub repository either using the command below or cloning the repo with the web URL. +We _highly_ recommend the use of VSCode for this project. Please download the VSCode and clone the GitHub repository either using the command below or cloning the repo with the web URL. ```sh git clone git@github.com:SGAOperations/website-development.git diff --git a/database.json b/database.json index 4d47ae4f..6e033ddd 100644 --- a/database.json +++ b/database.json @@ -1,821 +1,821 @@ { - "/": { - "root": { - "props": { - "title": "" - } - }, - "content": [ - { - "type": "Header", - "props": { - "nav": [ - { - "label": "Home", - "items": [] - }, - { - "label": "About", - "items": [ - { - "label": "What is SGA?", - "href": "/about" - }, - { - "label": "Structure", - "href": "/structure" - }, - { - "label": "Divisions", - "href": "/divisions" - }, - { - "label": "Leadership", - "href": "/leadership" - }, - { - "label": "Our Projects", - "href": "/projects" - }, - { - "label": "Meeting Minutes", - "href": "/meetings" - }, - { - "label": "Governing Documents", - "href": "/documents" - }, - { - "label": "In The Press", - "href": "/press" - }, - { - "label": "Alumni", - "href": "/alumni" - } - ] - }, - { - "label": "Get Involved", - "items": [ - { - "label": "Overview", - "href": "/overview" - }, - { - "label": "Join a Committee", - "href": "/committees" - }, - { - "label": "Join a Board", - "href": "/boards" - }, - { - "label": "Join a Working Group", - "href": "/working-groups" - }, - { - "label": "Become a Senator", - "href": "/senators" - }, - { - "label": "Leadership and Board Applications", - "href": "/leadership-and-board" - }, - { - "label": "Mailing List", - "href": "/mailing-list" - } - ] - }, - { - "label": "Senate", - "items": [ - { - "label": "About", - "href": "/about" - }, - { - "label": "Legislation", - "href": "/legislation" - }, - { - "label": "Points of Information", - "href": "/points-of-information" - }, - { - "label": "Resources", - "href": "/resources" - } - ] - }, - { - "label": "Executive Branch", - "items": [ - { - "label": "Office of the President", - "href": "/office-of-the-president" - }, - { - "label": "Academic Affairs", - "href": "/academic-affairs" - }, - { - "label": "Campus Affairs", - "href": "/campus-affairs" - }, - { - "label": "Diversity, Equity, and Inclusion", - "href": "/diversity-equity-inclusion" - }, - { - "label": "External Affairs", - "href": "/external-affairs" - }, - { - "label": "Student Involvement", - "href": "/student-involvement" - }, - { - "label": "Student Success", - "href": "/student-success" - }, - { - "label": "Operational Affairs", - "href": "/operational-affairs" - } - ] - }, - { - "label": "Judicial Branch", - "items": [ - { - "label": "Operational Appeals Board", - "href": "/operational-appeals-board" - }, - { - "label": "Appeal Process", - "href": "/appeal-process" - } - ] - }, - { - "label": "Elections", - "items": [ - { - "label": "Elections Board", - "href": "/elections-board" - }, - { - "label": "Election", - "href": "/election" - }, - { - "label": "Referenda", - "href": "/referenda" - }, - { - "label": "Past Elections", - "href": "/past-elections" - }, - { - "label": "How to Run", - "href": "/how-to-run" - } - ] - }, - { - "label": "Student Organizations", - "items": [ - { - "label": "Overview", - "href": "/overview" - }, - { - "label": "Student Involvement", - "href": "/student-involvement" - }, - { - "label": "Finance Board", - "href": "/finance-board" - }, - { - "label": "Starting an Organization", - "href": "/starting-an-organization" - }, - { - "label": "Funding", - "href": "/funding" - }, - { - "label": "Policies", - "href": "/policies" - }, - { - "label": "Resources", - "href": "/resources" - } - ] - } - ], - "id": "Header-1b257ff1-798d-4e5e-8e92-7c1ce562968f" + "/": { + "root": { + "props": { + "title": "" + } + }, + "content": [ + { + "type": "Header", + "props": { + "nav": [ + { + "label": "Home", + "items": [] + }, + { + "label": "About", + "items": [ + { + "label": "What is SGA?", + "href": "/about" + }, + { + "label": "Structure", + "href": "/structure" + }, + { + "label": "Divisions", + "href": "/divisions" + }, + { + "label": "Leadership", + "href": "/leadership" + }, + { + "label": "Our Projects", + "href": "/projects" + }, + { + "label": "Meeting Minutes", + "href": "/meetings" + }, + { + "label": "Governing Documents", + "href": "/documents" + }, + { + "label": "In The Press", + "href": "/press" + }, + { + "label": "Alumni", + "href": "/alumni" + } + ] + }, + { + "label": "Get Involved", + "items": [ + { + "label": "Overview", + "href": "/overview" + }, + { + "label": "Join a Committee", + "href": "/committees" + }, + { + "label": "Join a Board", + "href": "/boards" + }, + { + "label": "Join a Working Group", + "href": "/working-groups" + }, + { + "label": "Become a Senator", + "href": "/senators" + }, + { + "label": "Leadership and Board Applications", + "href": "/leadership-and-board" + }, + { + "label": "Mailing List", + "href": "/mailing-list" + } + ] + }, + { + "label": "Senate", + "items": [ + { + "label": "About", + "href": "/about" + }, + { + "label": "Legislation", + "href": "/legislation" + }, + { + "label": "Points of Information", + "href": "/points-of-information" + }, + { + "label": "Resources", + "href": "/resources" + } + ] + }, + { + "label": "Executive Branch", + "items": [ + { + "label": "Office of the President", + "href": "/office-of-the-president" + }, + { + "label": "Academic Affairs", + "href": "/academic-affairs" + }, + { + "label": "Campus Affairs", + "href": "/campus-affairs" + }, + { + "label": "Diversity, Equity, and Inclusion", + "href": "/diversity-equity-inclusion" + }, + { + "label": "External Affairs", + "href": "/external-affairs" + }, + { + "label": "Student Involvement", + "href": "/student-involvement" + }, + { + "label": "Student Success", + "href": "/student-success" + }, + { + "label": "Operational Affairs", + "href": "/operational-affairs" + } + ] + }, + { + "label": "Judicial Branch", + "items": [ + { + "label": "Operational Appeals Board", + "href": "/operational-appeals-board" + }, + { + "label": "Appeal Process", + "href": "/appeal-process" } + ] + }, + { + "label": "Elections", + "items": [ + { + "label": "Elections Board", + "href": "/elections-board" + }, + { + "label": "Election", + "href": "/election" + }, + { + "label": "Referenda", + "href": "/referenda" + }, + { + "label": "Past Elections", + "href": "/past-elections" + }, + { + "label": "How to Run", + "href": "/how-to-run" + } + ] + }, + { + "label": "Student Organizations", + "items": [ + { + "label": "Overview", + "href": "/overview" + }, + { + "label": "Student Involvement", + "href": "/student-involvement" + }, + { + "label": "Finance Board", + "href": "/finance-board" + }, + { + "label": "Starting an Organization", + "href": "/starting-an-organization" + }, + { + "label": "Funding", + "href": "/funding" + }, + { + "label": "Policies", + "href": "/policies" + }, + { + "label": "Resources", + "href": "/resources" + } + ] } - ], - "zones": {} + ], + "id": "Header-1b257ff1-798d-4e5e-8e92-7c1ce562968f" + } + } + ], + "zones": {} + }, + "/test": { + "root": { + "props": {} }, - "/test": { - "root": { - "props": {} - }, - "content": [ - { - "type": "HeaderContent", - "props": { - "backgroundColor": "rgba(0, 0, 0, 0.5)", - "padding": "p-5", - "titleText": "SGA", - "titleSize": "text-9xl", - "titleColor": "text-sga-red", - "logoSrc": "", - "logoAltText": "SGA Logo", - "logoWidth": "w-52", - "logoHeight": "h-auto", - "dividerColor": "bg-black", - "dividerWidth": "w-1", - "dividerHeight": "h-[180px]", - "dividerMargin": "mx-8", - "id": "HeaderContent-744262c0-24d7-4466-b0b3-93e520101fe9" + "content": [ + { + "type": "HeaderContent", + "props": { + "backgroundColor": "rgba(0, 0, 0, 0.5)", + "padding": "p-5", + "titleText": "SGA", + "titleSize": "text-9xl", + "titleColor": "text-sga-red", + "logoSrc": "", + "logoAltText": "SGA Logo", + "logoWidth": "w-52", + "logoHeight": "h-auto", + "dividerColor": "bg-black", + "dividerWidth": "w-1", + "dividerHeight": "h-[180px]", + "dividerMargin": "mx-8", + "id": "HeaderContent-744262c0-24d7-4466-b0b3-93e520101fe9" + } + }, + { + "type": "Navigation", + "props": { + "backgroundColor": "rgba(0, 0, 0, 0.5)", + "padding": "p-5", + "nav": [ + { + "label": "Home", + "items": [] + }, + { + "label": "About", + "items": [ + { + "label": "What is SGA?", + "href": "/about" + }, + { + "label": "Structure", + "href": "/structure" + }, + { + "label": "Divisions", + "href": "/divisions" + }, + { + "label": "Leadership", + "href": "/leadership" + }, + { + "label": "Our Projects", + "href": "/projects" + }, + { + "label": "Meeting Minutes", + "href": "/meetings" + }, + { + "label": "Governing Documents", + "href": "/documents" + }, + { + "label": "In The Press", + "href": "/press" + }, + { + "label": "Alumni", + "href": "/alumni" + } + ] + }, + { + "label": "Get Involved", + "items": [ + { + "label": "Overview", + "href": "/overview" + }, + { + "label": "Join a Committee", + "href": "/committees" + }, + { + "label": "Join a Board", + "href": "/boards" + }, + { + "label": "Join a Working Group", + "href": "/working-groups" + }, + { + "label": "Become a Senator", + "href": "/senators" + }, + { + "label": "Leadership and Board Applications", + "href": "/leadership-and-board" + }, + { + "label": "Mailing List", + "href": "/mailing-list" } + ] }, { - "type": "Navigation", - "props": { - "backgroundColor": "rgba(0, 0, 0, 0.5)", - "padding": "p-5", - "nav": [ - { - "label": "Home", - "items": [] - }, - { - "label": "About", - "items": [ - { - "label": "What is SGA?", - "href": "/about" - }, - { - "label": "Structure", - "href": "/structure" - }, - { - "label": "Divisions", - "href": "/divisions" - }, - { - "label": "Leadership", - "href": "/leadership" - }, - { - "label": "Our Projects", - "href": "/projects" - }, - { - "label": "Meeting Minutes", - "href": "/meetings" - }, - { - "label": "Governing Documents", - "href": "/documents" - }, - { - "label": "In The Press", - "href": "/press" - }, - { - "label": "Alumni", - "href": "/alumni" - } - ] - }, - { - "label": "Get Involved", - "items": [ - { - "label": "Overview", - "href": "/overview" - }, - { - "label": "Join a Committee", - "href": "/committees" - }, - { - "label": "Join a Board", - "href": "/boards" - }, - { - "label": "Join a Working Group", - "href": "/working-groups" - }, - { - "label": "Become a Senator", - "href": "/senators" - }, - { - "label": "Leadership and Board Applications", - "href": "/leadership-and-board" - }, - { - "label": "Mailing List", - "href": "/mailing-list" - } - ] - }, - { - "label": "Senate", - "items": [ - { - "label": "About", - "href": "/about" - }, - { - "label": "Legislation", - "href": "/legislation" - }, - { - "label": "Points of Information", - "href": "/points-of-information" - }, - { - "label": "Resources", - "href": "/resources" - } - ] - }, - { - "label": "Executive Branch", - "items": [ - { - "label": "Office of the President", - "href": "/office-of-the-president" - }, - { - "label": "Academic Affairs", - "href": "/academic-affairs" - }, - { - "label": "Campus Affairs", - "href": "/campus-affairs" - }, - { - "label": "Diversity, Equity, and Inclusion", - "href": "/diversity-equity-inclusion" - }, - { - "label": "External Affairs", - "href": "/external-affairs" - }, - { - "label": "Student Involvement", - "href": "/student-involvement" - }, - { - "label": "Student Success", - "href": "/student-success" - }, - { - "label": "Operational Affairs", - "href": "/operational-affairs" - } - ] - }, - { - "label": "Judicial Branch", - "items": [ - { - "label": "Operational Appeals Board", - "href": "/operational-appeals-board" - }, - { - "label": "Appeal Process", - "href": "/appeal-process" - } - ] - }, - { - "label": "Elections", - "items": [ - { - "label": "Elections Board", - "href": "/elections-board" - }, - { - "label": "Election", - "href": "/election" - }, - { - "label": "Referenda", - "href": "/referenda" - }, - { - "label": "Past Elections", - "href": "/past-elections" - }, - { - "label": "How to Run", - "href": "/how-to-run" - } - ] - }, - { - "label": "Student Organizations", - "items": [ - { - "label": "Overview", - "href": "/overview" - }, - { - "label": "Student Involvement", - "href": "/student-involvement" - }, - { - "label": "Finance Board", - "href": "/finance-board" - }, - { - "label": "Starting an Organization", - "href": "/starting-an-organization" - }, - { - "label": "Funding", - "href": "/funding" - }, - { - "label": "Policies", - "href": "/policies" - }, - { - "label": "Resources", - "href": "/resources" - } - ] - } - ], - "id": "Navigation-7f2ad423-ae35-426c-9e9c-92e21db4f9d8" + "label": "Senate", + "items": [ + { + "label": "About", + "href": "/about" + }, + { + "label": "Legislation", + "href": "/legislation" + }, + { + "label": "Points of Information", + "href": "/points-of-information" + }, + { + "label": "Resources", + "href": "/resources" } + ] }, { - "type": "Footer", - "props": { - "backgroundColor": "#000000", - "padding": "p-10", - "showActionButtons": true, - "actionButton1Text": "GIVE FEEDBACK", - "actionButton1Href": "#", - "actionButton2Text": "MAILING LIST", - "actionButton2Href": "#", - "actionButton3Text": "GET INVOLVED", - "actionButton3Href": "#", - "showSocialMedia": true, - "socialMedia1Label": "SGA", - "socialMedia1LogoSrc": "", - "socialMedia1LogoAlt": "SGA Instagram", - "socialMedia2Label": "Campus Affairs", - "socialMedia2LogoSrc": "", - "socialMedia2LogoAlt": "Campus Affairs Instagram", - "socialMedia3Label": "SGA", - "socialMedia3LogoSrc": "", - "socialMedia3LogoAlt": "SGA TikTok", - "organizationName": "Northeastern University Student Government Association", - "organizationAddress": "332 Curry Student Center, 360 Huntington Avenue, Boston, MA 02115", - "webmasterEmail": "sgaOperations@northeastern.edu", - "webmasterLabel": "Webmaster:", - "mediaInquiriesEmail": "sgaExternalAffairs@northeastern.edu", - "mediaInquiriesLabel": "Media Inquiries:", - "id": "Footer-34d7a082-8853-4f2d-8257-d04c83c00321" + "label": "Executive Branch", + "items": [ + { + "label": "Office of the President", + "href": "/office-of-the-president" + }, + { + "label": "Academic Affairs", + "href": "/academic-affairs" + }, + { + "label": "Campus Affairs", + "href": "/campus-affairs" + }, + { + "label": "Diversity, Equity, and Inclusion", + "href": "/diversity-equity-inclusion" + }, + { + "label": "External Affairs", + "href": "/external-affairs" + }, + { + "label": "Student Involvement", + "href": "/student-involvement" + }, + { + "label": "Student Success", + "href": "/student-success" + }, + { + "label": "Operational Affairs", + "href": "/operational-affairs" } + ] + }, + { + "label": "Judicial Branch", + "items": [ + { + "label": "Operational Appeals Board", + "href": "/operational-appeals-board" + }, + { + "label": "Appeal Process", + "href": "/appeal-process" + } + ] + }, + { + "label": "Elections", + "items": [ + { + "label": "Elections Board", + "href": "/elections-board" + }, + { + "label": "Election", + "href": "/election" + }, + { + "label": "Referenda", + "href": "/referenda" + }, + { + "label": "Past Elections", + "href": "/past-elections" + }, + { + "label": "How to Run", + "href": "/how-to-run" + } + ] + }, + { + "label": "Student Organizations", + "items": [ + { + "label": "Overview", + "href": "/overview" + }, + { + "label": "Student Involvement", + "href": "/student-involvement" + }, + { + "label": "Finance Board", + "href": "/finance-board" + }, + { + "label": "Starting an Organization", + "href": "/starting-an-organization" + }, + { + "label": "Funding", + "href": "/funding" + }, + { + "label": "Policies", + "href": "/policies" + }, + { + "label": "Resources", + "href": "/resources" + } + ] } - ], - "zones": {} + ], + "id": "Navigation-7f2ad423-ae35-426c-9e9c-92e21db4f9d8" + } + }, + { + "type": "Footer", + "props": { + "backgroundColor": "#000000", + "padding": "p-10", + "showActionButtons": true, + "actionButton1Text": "GIVE FEEDBACK", + "actionButton1Href": "#", + "actionButton2Text": "MAILING LIST", + "actionButton2Href": "#", + "actionButton3Text": "GET INVOLVED", + "actionButton3Href": "#", + "showSocialMedia": true, + "socialMedia1Label": "SGA", + "socialMedia1LogoSrc": "", + "socialMedia1LogoAlt": "SGA Instagram", + "socialMedia2Label": "Campus Affairs", + "socialMedia2LogoSrc": "", + "socialMedia2LogoAlt": "Campus Affairs Instagram", + "socialMedia3Label": "SGA", + "socialMedia3LogoSrc": "", + "socialMedia3LogoAlt": "SGA TikTok", + "organizationName": "Northeastern University Student Government Association", + "organizationAddress": "332 Curry Student Center, 360 Huntington Avenue, Boston, MA 02115", + "webmasterEmail": "sgaOperations@northeastern.edu", + "webmasterLabel": "Webmaster:", + "mediaInquiriesEmail": "sgaExternalAffairs@northeastern.edu", + "mediaInquiriesLabel": "Media Inquiries:", + "id": "Footer-34d7a082-8853-4f2d-8257-d04c83c00321" + } + } + ], + "zones": {} + }, + "/join-a-committee": { + "root": { + "props": {} }, - "/join-a-committee": { - "root": { - "props": {} - }, - "content": [ - { - "type": "HeaderContent", - "props": { - "backgroundColor": "rgba(0, 0, 0, 0.5)", - "padding": "p-5", - "titleText": "SGA", - "titleSize": "text-9xl", - "titleColor": "text-sga-red", - "logoSrc": "", - "logoAltText": "SGA Logo", - "logoWidth": "w-52", - "logoHeight": "h-auto", - "dividerColor": "bg-black", - "dividerWidth": "w-1", - "dividerHeight": "h-[180px]", - "dividerMargin": "mx-8", - "id": "HeaderContent-fe73daef-9ce9-48a3-9b48-df934e5acf49" + "content": [ + { + "type": "HeaderContent", + "props": { + "backgroundColor": "rgba(0, 0, 0, 0.5)", + "padding": "p-5", + "titleText": "SGA", + "titleSize": "text-9xl", + "titleColor": "text-sga-red", + "logoSrc": "", + "logoAltText": "SGA Logo", + "logoWidth": "w-52", + "logoHeight": "h-auto", + "dividerColor": "bg-black", + "dividerWidth": "w-1", + "dividerHeight": "h-[180px]", + "dividerMargin": "mx-8", + "id": "HeaderContent-fe73daef-9ce9-48a3-9b48-df934e5acf49" + } + }, + { + "type": "Navigation", + "props": { + "backgroundColor": "rgba(0, 0, 0, 0.5)", + "padding": "p-5", + "nav": [ + { + "label": "Home", + "items": [] + }, + { + "label": "About", + "items": [ + { + "label": "What is SGA?", + "href": "/about" + }, + { + "label": "Structure", + "href": "/structure" + }, + { + "label": "Divisions", + "href": "/divisions" + }, + { + "label": "Leadership", + "href": "/leadership" + }, + { + "label": "Our Projects", + "href": "/projects" + }, + { + "label": "Meeting Minutes", + "href": "/meetings" + }, + { + "label": "Governing Documents", + "href": "/documents" + }, + { + "label": "In The Press", + "href": "/press" + }, + { + "label": "Alumni", + "href": "/alumni" } + ] }, { - "type": "Navigation", - "props": { - "backgroundColor": "rgba(0, 0, 0, 0.5)", - "padding": "p-5", - "nav": [ - { - "label": "Home", - "items": [] - }, - { - "label": "About", - "items": [ - { - "label": "What is SGA?", - "href": "/about" - }, - { - "label": "Structure", - "href": "/structure" - }, - { - "label": "Divisions", - "href": "/divisions" - }, - { - "label": "Leadership", - "href": "/leadership" - }, - { - "label": "Our Projects", - "href": "/projects" - }, - { - "label": "Meeting Minutes", - "href": "/meetings" - }, - { - "label": "Governing Documents", - "href": "/documents" - }, - { - "label": "In The Press", - "href": "/press" - }, - { - "label": "Alumni", - "href": "/alumni" - } - ] - }, - { - "label": "Get Involved", - "items": [ - { - "label": "Overview", - "href": "/overview" - }, - { - "label": "Join a Committee", - "href": "/committees" - }, - { - "label": "Join a Board", - "href": "/boards" - }, - { - "label": "Join a Working Group", - "href": "/working-groups" - }, - { - "label": "Become a Senator", - "href": "/senators" - }, - { - "label": "Leadership and Board Applications", - "href": "/leadership-and-board" - }, - { - "label": "Mailing List", - "href": "/mailing-list" - } - ] - }, - { - "label": "Senate", - "items": [ - { - "label": "About", - "href": "/about" - }, - { - "label": "Legislation", - "href": "/legislation" - }, - { - "label": "Points of Information", - "href": "/points-of-information" - }, - { - "label": "Resources", - "href": "/resources" - } - ] - }, - { - "label": "Executive Branch", - "items": [ - { - "label": "Office of the President", - "href": "/office-of-the-president" - }, - { - "label": "Academic Affairs", - "href": "/academic-affairs" - }, - { - "label": "Campus Affairs", - "href": "/campus-affairs" - }, - { - "label": "Diversity, Equity, and Inclusion", - "href": "/diversity-equity-inclusion" - }, - { - "label": "External Affairs", - "href": "/external-affairs" - }, - { - "label": "Student Involvement", - "href": "/student-involvement" - }, - { - "label": "Student Success", - "href": "/student-success" - }, - { - "label": "Operational Affairs", - "href": "/operational-affairs" - } - ] - }, - { - "label": "Judicial Branch", - "items": [ - { - "label": "Operational Appeals Board", - "href": "/operational-appeals-board" - }, - { - "label": "Appeal Process", - "href": "/appeal-process" - } - ] - }, - { - "label": "Elections", - "items": [ - { - "label": "Elections Board", - "href": "/elections-board" - }, - { - "label": "Election", - "href": "/election" - }, - { - "label": "Referenda", - "href": "/referenda" - }, - { - "label": "Past Elections", - "href": "/past-elections" - }, - { - "label": "How to Run", - "href": "/how-to-run" - } - ] - }, - { - "label": "Student Organizations", - "items": [ - { - "label": "Overview", - "href": "/overview" - }, - { - "label": "Student Involvement", - "href": "/student-involvement" - }, - { - "label": "Finance Board", - "href": "/finance-board" - }, - { - "label": "Starting an Organization", - "href": "/starting-an-organization" - }, - { - "label": "Funding", - "href": "/funding" - }, - { - "label": "Policies", - "href": "/policies" - }, - { - "label": "Resources", - "href": "/resources" - } - ] - } - ], - "id": "Navigation-e7fcbeb1-c482-4370-8f9b-f1037a4ea698" + "label": "Get Involved", + "items": [ + { + "label": "Overview", + "href": "/overview" + }, + { + "label": "Join a Committee", + "href": "/committees" + }, + { + "label": "Join a Board", + "href": "/boards" + }, + { + "label": "Join a Working Group", + "href": "/working-groups" + }, + { + "label": "Become a Senator", + "href": "/senators" + }, + { + "label": "Leadership and Board Applications", + "href": "/leadership-and-board" + }, + { + "label": "Mailing List", + "href": "/mailing-list" } + ] }, { - "type": "PageHeader", - "props": { - "title": "JOIN A COMMITTEE", - "subtitle": "", - "description": "SGA Committees meet once a week to focus on a specific aspect of the student experience and work on projects related to improving an aspect of the university. They are open to all undergraduate Northeastern students on the Boston Campus, and no experience or commitment is necessary!", - "additionalText": "To join a committee just show up! Find more information about each committee at the links below.", - "id": "PageHeader-5a3143eb-8de3-4b08-b375-352078faa430" + "label": "Senate", + "items": [ + { + "label": "About", + "href": "/about" + }, + { + "label": "Legislation", + "href": "/legislation" + }, + { + "label": "Points of Information", + "href": "/points-of-information" + }, + { + "label": "Resources", + "href": "/resources" } + ] }, { - "type": "CommitteeGrid", - "props": { - "title": "COMMITTEE MEETINGS", - "committees": [ - { - "value": "ACADEMIC AFFAIRS" - }, - { - "value": "CAMPUS SERVICES" - }, - { - "value": "SUSTAINABILITY" - }, - { - "value": "GLOBAL EXPERIENCE" - }, - { - "value": "WELLNESS" - }, - { - "value": "STUDENT ENGAGEMENT" - }, - { - "value": "DIVERSITY, EQUITY, AND INCLUSION" - }, - { - "value": "COMMUNICATIONS AND EVENTS" - }, - { - "value": "STUDENT ORGANIZATION OPERATIONS" - } - ], - "id": "CommitteeGrid-1696f6ba-8251-4962-a820-d4e88ef8db38" + "label": "Executive Branch", + "items": [ + { + "label": "Office of the President", + "href": "/office-of-the-president" + }, + { + "label": "Academic Affairs", + "href": "/academic-affairs" + }, + { + "label": "Campus Affairs", + "href": "/campus-affairs" + }, + { + "label": "Diversity, Equity, and Inclusion", + "href": "/diversity-equity-inclusion" + }, + { + "label": "External Affairs", + "href": "/external-affairs" + }, + { + "label": "Student Involvement", + "href": "/student-involvement" + }, + { + "label": "Student Success", + "href": "/student-success" + }, + { + "label": "Operational Affairs", + "href": "/operational-affairs" } + ] }, { - "type": "Footer", - "props": { - "showActionButtons": true, - "actionButton1Text": "GIVE FEEDBACK", - "actionButton1Href": "#", - "actionButton2Text": "MAILING LIST", - "actionButton2Href": "#", - "actionButton3Text": "GET INVOLVED", - "actionButton3Href": "#", - "showSocialMedia": true, - "socialMedia1Label": "SGA", - "socialMedia1LogoSrc": "", - "socialMedia1LogoAlt": "SGA Instagram", - "socialMedia2Label": "Campus Affairs", - "socialMedia2LogoSrc": "", - "socialMedia2LogoAlt": "Campus Affairs Instagram", - "socialMedia3Label": "SGA", - "socialMedia3LogoSrc": "", - "socialMedia3LogoAlt": "SGA TikTok", - "organizationName": "Northeastern University Student Government Association", - "organizationAddress": "332 Curry Student Center, 360 Huntington Avenue, Boston, MA 02115", - "webmasterEmail": "sgaOperations@northeastern.edu", - "webmasterLabel": "Webmaster:", - "mediaInquiriesEmail": "sgaExternalAffairs@northeastern.edu", - "mediaInquiriesLabel": "Media Inquiries:", - "id": "Footer-00e51f60-d0c9-4fe8-a6c2-1c28221e9161" + "label": "Judicial Branch", + "items": [ + { + "label": "Operational Appeals Board", + "href": "/operational-appeals-board" + }, + { + "label": "Appeal Process", + "href": "/appeal-process" } + ] + }, + { + "label": "Elections", + "items": [ + { + "label": "Elections Board", + "href": "/elections-board" + }, + { + "label": "Election", + "href": "/election" + }, + { + "label": "Referenda", + "href": "/referenda" + }, + { + "label": "Past Elections", + "href": "/past-elections" + }, + { + "label": "How to Run", + "href": "/how-to-run" + } + ] + }, + { + "label": "Student Organizations", + "items": [ + { + "label": "Overview", + "href": "/overview" + }, + { + "label": "Student Involvement", + "href": "/student-involvement" + }, + { + "label": "Finance Board", + "href": "/finance-board" + }, + { + "label": "Starting an Organization", + "href": "/starting-an-organization" + }, + { + "label": "Funding", + "href": "/funding" + }, + { + "label": "Policies", + "href": "/policies" + }, + { + "label": "Resources", + "href": "/resources" + } + ] + } + ], + "id": "Navigation-e7fcbeb1-c482-4370-8f9b-f1037a4ea698" + } + }, + { + "type": "PageHeader", + "props": { + "title": "JOIN A COMMITTEE", + "subtitle": "", + "description": "SGA Committees meet once a week to focus on a specific aspect of the student experience and work on projects related to improving an aspect of the university. They are open to all undergraduate Northeastern students on the Boston Campus, and no experience or commitment is necessary!", + "additionalText": "To join a committee just show up! Find more information about each committee at the links below.", + "id": "PageHeader-5a3143eb-8de3-4b08-b375-352078faa430" + } + }, + { + "type": "CommitteeGrid", + "props": { + "title": "COMMITTEE MEETINGS", + "committees": [ + { + "value": "ACADEMIC AFFAIRS" + }, + { + "value": "CAMPUS SERVICES" + }, + { + "value": "SUSTAINABILITY" + }, + { + "value": "GLOBAL EXPERIENCE" + }, + { + "value": "WELLNESS" + }, + { + "value": "STUDENT ENGAGEMENT" + }, + { + "value": "DIVERSITY, EQUITY, AND INCLUSION" + }, + { + "value": "COMMUNICATIONS AND EVENTS" + }, + { + "value": "STUDENT ORGANIZATION OPERATIONS" } - ], - "zones": {} - } -} \ No newline at end of file + ], + "id": "CommitteeGrid-1696f6ba-8251-4962-a820-d4e88ef8db38" + } + }, + { + "type": "Footer", + "props": { + "showActionButtons": true, + "actionButton1Text": "GIVE FEEDBACK", + "actionButton1Href": "#", + "actionButton2Text": "MAILING LIST", + "actionButton2Href": "#", + "actionButton3Text": "GET INVOLVED", + "actionButton3Href": "#", + "showSocialMedia": true, + "socialMedia1Label": "SGA", + "socialMedia1LogoSrc": "", + "socialMedia1LogoAlt": "SGA Instagram", + "socialMedia2Label": "Campus Affairs", + "socialMedia2LogoSrc": "", + "socialMedia2LogoAlt": "Campus Affairs Instagram", + "socialMedia3Label": "SGA", + "socialMedia3LogoSrc": "", + "socialMedia3LogoAlt": "SGA TikTok", + "organizationName": "Northeastern University Student Government Association", + "organizationAddress": "332 Curry Student Center, 360 Huntington Avenue, Boston, MA 02115", + "webmasterEmail": "sgaOperations@northeastern.edu", + "webmasterLabel": "Webmaster:", + "mediaInquiriesEmail": "sgaExternalAffairs@northeastern.edu", + "mediaInquiriesLabel": "Media Inquiries:", + "id": "Footer-00e51f60-d0c9-4fe8-a6c2-1c28221e9161" + } + } + ], + "zones": {} + } +} diff --git a/docs/NEON-MIGRATION-INVESTIGATION.md b/docs/NEON-MIGRATION-INVESTIGATION.md index e36552fb..7342a798 100644 --- a/docs/NEON-MIGRATION-INVESTIGATION.md +++ b/docs/NEON-MIGRATION-INVESTIGATION.md @@ -1,9 +1,9 @@ # Supabase to Neon Migration Assessment -| | | -| --- | --- | -| Author | Shlok Patel | -| Date | October 2, 2026 | +| | | +| ------ | ---------------- | +| Author | Shlok Patel | +| Date | October 2, 2026 | | Status | Draft for review | ## 1. Purpose @@ -23,10 +23,10 @@ Neon provides managed PostgreSQL but does not offer object storage. A full move ## 3. Options and Estimated Effort -| Option | Scope | Estimate | -| --- | --- | --- | -| A | Move the database to Neon; keep Supabase Storage for media | 0.5 to 1 day | -| B | Move the database to Neon and media to a separate storage provider | 2 to 4 days | +| Option | Scope | Estimate | +| ------ | ------------------------------------------------------------------ | ------------ | +| A | Move the database to Neon; keep Supabase Storage for media | 0.5 to 1 day | +| B | Move the database to Neon and media to a separate storage provider | 2 to 4 days | Both estimates exclude documentation updates, changes to the local development setup, and testing, which together add 0.5 to 1 day. @@ -50,15 +50,15 @@ Both estimates exclude documentation updates, changes to the local development s ## 5. Risks -| ID | Risk | Impact | Mitigation | -| --- | --- | --- | --- | -| R1 | Saved page content (`Version.content`) stores the full public URL of each image. If media moves to a new provider, existing pages, including published ones, will keep referencing Supabase. | High (Option B) | Before migrating, change the Media block to store only `mediaId` and resolve the URL at render time. Otherwise, run a one-time data migration to rewrite stored URLs. | -| R2 | Neon has no local emulator equivalent to `supabase start`, so the local setup changes for every developer. | Medium | Document a standard local setup and validate it with the team before cutover. | -| R3 | On the free tier, Neon suspends idle databases. The first request after a period of inactivity can take up to about one second longer. | Medium | Disable auto-suspend on a paid plan, or accept the latency for this workload. | -| R4 | Neon's pooled connections use PgBouncer in transaction mode, which does not support Prisma migrations. | Low | Keep migrations on `DIRECT_URL`. The current configuration already separates the two connections. | -| R5 | The Neon free tier is limited to 0.5 GB of storage. Each page save writes a full JSON copy to `Version`, so storage use grows over time. | Low | Monitor usage and consider a retention policy for old versions. | -| R6 | Upload limits are inconsistent. The bucket accepts files up to 50 MiB, server actions are capped at 10 MB, and Vercel functions limit request bodies to about 4.5 MB. | Medium (Option B) | Align the limits. If larger files are needed, upload directly from the client to the storage provider. | -| R7 | Planned authentication work may assume Supabase Auth. | Low | Choose the authentication provider before the migration to avoid a second move. | +| ID | Risk | Impact | Mitigation | +| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| R1 | Saved page content (`Version.content`) stores the full public URL of each image. If media moves to a new provider, existing pages, including published ones, will keep referencing Supabase. | High (Option B) | Before migrating, change the Media block to store only `mediaId` and resolve the URL at render time. Otherwise, run a one-time data migration to rewrite stored URLs. | +| R2 | Neon has no local emulator equivalent to `supabase start`, so the local setup changes for every developer. | Medium | Document a standard local setup and validate it with the team before cutover. | +| R3 | On the free tier, Neon suspends idle databases. The first request after a period of inactivity can take up to about one second longer. | Medium | Disable auto-suspend on a paid plan, or accept the latency for this workload. | +| R4 | Neon's pooled connections use PgBouncer in transaction mode, which does not support Prisma migrations. | Low | Keep migrations on `DIRECT_URL`. The current configuration already separates the two connections. | +| R5 | The Neon free tier is limited to 0.5 GB of storage. Each page save writes a full JSON copy to `Version`, so storage use grows over time. | Low | Monitor usage and consider a retention policy for old versions. | +| R6 | Upload limits are inconsistent. The bucket accepts files up to 50 MiB, server actions are capped at 10 MB, and Vercel functions limit request bodies to about 4.5 MB. | Medium (Option B) | Align the limits. If larger files are needed, upload directly from the client to the storage provider. | +| R7 | Planned authentication work may assume Supabase Auth. | Low | Choose the authentication provider before the migration to avoid a second move. | ## 6. Recommendation diff --git a/docs/STYLING.md b/docs/STYLING.md index 32c8837d..2f91e5c1 100644 --- a/docs/STYLING.md +++ b/docs/STYLING.md @@ -18,7 +18,7 @@ const padding = defineToken({ md: { label: "Medium", classes: "p-4" }, }); -padding.classes // → { sm: "p-2", md: "p-4" } +padding.classes; // → { sm: "p-2", md: "p-4" } ``` You can add extra columns beyond `classes` — any property besides `label` becomes a `Record` on the token: @@ -29,8 +29,8 @@ const spacing = defineToken({ lg: { label: "Large", classes: "gap-6", px: 24 }, }); -spacing.classes // → { sm: "gap-2", lg: "gap-6" } -spacing.px // → { sm: 8, lg: 24 } +spacing.classes; // → { sm: "gap-2", lg: "gap-6" } +spacing.px; // → { sm: 8, lg: 24 } ``` **Plain tokens** use simple string values (key = value, string = editor label). Useful when there are no Tailwind classes to map to: @@ -38,15 +38,15 @@ spacing.px // → { sm: 8, lg: 24 } ```ts const variant = defineToken({ default: "Default", outline: "Outline" }); -variant.options // → [{ value: "default", label: "Default" }, ...] -variant.defaultValue // → "default" +variant.options; // → [{ value: "default", label: "Default" }, ...] +variant.defaultValue; // → "default" ``` The first key is the default unless you pass an explicit second argument: ```ts const size = defineToken({ sm: "Small", md: "Medium", lg: "Large" }, "md"); -size.defaultValue // → "md" +size.defaultValue; // → "md" ``` Use `TokenValue` to extract a token's value union for type annotations: @@ -69,12 +69,12 @@ type ResponsiveValue = { base: T } & Partial>; **Helper functions** for working with responsive values: -| Function | Purpose | -|---|---| -| `resolveAt(value, breakpoint)` | Returns the effective value at a breakpoint, falling back to the nearest smaller one. `resolveAt({ base: "sm", lg: "lg" }, "md")` → `"sm"` | -| `hasOverride(value)` | Returns `true` if any non-base overrides are set | -| `map(value, fn)` | Transforms each set breakpoint. `map(value, (v) => v.toUpperCase())` | -| `setAt(value, breakpoint, newVal)` | Returns a new `ResponsiveValue` with one breakpoint changed. Pass `undefined` to clear an override | +| Function | Purpose | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `resolveAt(value, breakpoint)` | Returns the effective value at a breakpoint, falling back to the nearest smaller one. `resolveAt({ base: "sm", lg: "lg" }, "md")` → `"sm"` | +| `hasOverride(value)` | Returns `true` if any non-base overrides are set | +| `map(value, fn)` | Transforms each set breakpoint. `map(value, (v) => v.toUpperCase())` | +| `setAt(value, breakpoint, newVal)` | Returns a new `ResponsiveValue` with one breakpoint changed. Pass `undefined` to clear an override | These helpers are for component logic — see [Rendering](#4-rendering) for how responsive values become Tailwind classes. @@ -90,8 +90,8 @@ const props = defineProps({ content: field.slot(), // Static select / radio — token-backed, single value - radius: field.select(radius, { label: "Corners" }), - layout: field.radio(layout, { label: "Layout" }), + radius: field.select(radius, { label: "Corners" }), + layout: field.radio(layout, { label: "Layout" }), // Responsive — per-breakpoint picker, also token-backed padding: responsive.select(padding, { label: "Padding" }), @@ -106,24 +106,27 @@ const props = defineProps({ }), // Raw — any Puck field descriptor, for things tokens don't cover - url: field.raw({ type: "text", label: "URL" }, ""), + url: field.raw({ type: "text", label: "URL" }, ""), }); ``` **Defaults.** Every builder falls back to the token's first key if no `default` is given. You can override with a scalar or a full responsive object: ```ts -responsive.select(columnCount, { label: "Columns", default: "3" }) -responsive.select(columnCount, { label: "Columns", default: { base: "1", md: "3" } }) -responsive.number({ label: "Rows", default: 2, min: 1, max: 6, step: 1 }) -responsive.number({ label: "Rows", default: { base: 1, md: 3 } }) +responsive.select(columnCount, { label: "Columns", default: "3" }); +responsive.select(columnCount, { + label: "Columns", + default: { base: "1", md: "3" }, +}); +responsive.number({ label: "Rows", default: 2, min: 1, max: 6, step: 1 }); +responsive.number({ label: "Rows", default: { base: 1, md: 3 } }); ``` **Slots** accept an optional allow/disallow list to restrict which child components can be dropped in: ```ts -field.slot({ allow: ["Card", "Button"] }) -field.slot({ disallow: ["Section"] }) +field.slot({ allow: ["Card", "Button"] }); +field.slot({ disallow: ["Section"] }); ``` The same token definition works for both static and responsive use. Numeric responsive fields do not need a token. @@ -145,7 +148,7 @@ To add a new responsive field kind: 1. Add the descriptor type and factory to `kinds.ts`. 2. Add a `-field.tsx` renderer and register it in `client.tsx`. -4. Optionally add a convenience builder in `src/lib/puck/define-props.ts`. +3. Optionally add a convenience builder in `src/lib/puck/define-props.ts`. ### 4. Rendering @@ -153,10 +156,10 @@ Static tokens look up the class map directly. Responsive tokens go through `reso ```ts // Static — direct lookup -radius.classes[r] // → "rounded-md" +radius.classes[r]; // → "rounded-md" // Responsive — produces prefixed classes for each set breakpoint -resolveResponsive(padding, paddingToken.classes) +resolveResponsive(padding, paddingToken.classes); // { base: "sm", md: "lg" } → "p-2 md:p-6" ``` @@ -168,8 +171,8 @@ Combine everything with `cn()` (re-exported from `src/lib/utils`): ```ts const classes = cn( "w-full", - bgColor.classes[bg], // static - resolveResponsive(py, paddingY.classes), // responsive + bgColor.classes[bg], // static + resolveResponsive(py, paddingY.classes), // responsive ); ``` @@ -198,9 +201,9 @@ If you need a new token, add it to `tokens.ts` and export a type alias: ```ts export const fontSize = defineToken({ - sm: { label: "Small", classes: "text-sm" }, - base: { label: "Base", classes: "text-base" }, - lg: { label: "Large", classes: "text-lg" }, + sm: { label: "Small", classes: "text-sm" }, + base: { label: "Base", classes: "text-base" }, + lg: { label: "Large", classes: "text-lg" }, }); export type FontSize = TokenValue; @@ -227,7 +230,13 @@ type CardProps = { const props = defineProps({ content: field.slot(), padding: responsive.select(padding, { label: "Padding", default: "md" }), - columns: responsive.number({ label: "Columns", default: { base: 1, md: 2 }, min: 1, max: 4, step: 1 }), + columns: responsive.number({ + label: "Columns", + default: { base: 1, md: 2 }, + min: 1, + max: 4, + step: 1, + }), bgColor: field.select(bgColor, { label: "Background" }), title: field.raw({ type: "text", label: "Title" }, ""), }); diff --git a/docs/SUPABASE_SETUP.md b/docs/SUPABASE_SETUP.md index ea02b356..41005c12 100644 --- a/docs/SUPABASE_SETUP.md +++ b/docs/SUPABASE_SETUP.md @@ -49,13 +49,13 @@ These credentials are also accessible again via `npx supabase status` if you nee Copy `.env.example` to `.env` and fill in the credentials from the output above: -| Variable | Source | -| --- | --- | -| `DATABASE_URL` | Database "URL" — used by the application layer (configure connection pooling via pgbouncer in prod) | -| `DIRECT_URL` | Database "URL" — used by Prisma for migrations (direct connection, no pooling) | -| `SUPABASE_URL` | "Project URL" | -| `SUPABASE_ANON_KEY` | "Publishable" key | -| `SUPABASE_SERVICE_ROLE_KEY` | "Secret" key (**never expose to the client**) | +| Variable | Source | +| --------------------------- | --------------------------------------------------------------------------------------------------- | +| `DATABASE_URL` | Database "URL" — used by the application layer (configure connection pooling via pgbouncer in prod) | +| `DIRECT_URL` | Database "URL" — used by Prisma for migrations (direct connection, no pooling) | +| `SUPABASE_URL` | "Project URL" | +| `SUPABASE_ANON_KEY` | "Publishable" key | +| `SUPABASE_SERVICE_ROLE_KEY` | "Secret" key (**never expose to the client**) | > Locally there is no connection pooling, so `DATABASE_URL` and `DIRECT_URL` will be the same. diff --git a/prisma.config.ts b/prisma.config.ts index 7fdb1a9b..b92eafbb 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -2,12 +2,12 @@ import "dotenv/config"; import { defineConfig } from "prisma/config"; export default defineConfig({ - schema: "prisma/schema.prisma", - migrations: { - path: "prisma/migrations", - seed: "tsx prisma/seed.ts", - }, - datasource: { - url: process.env.DIRECT_URL ?? "", - }, + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + seed: "tsx prisma/seed.ts", + }, + datasource: { + url: process.env.DIRECT_URL ?? "", + }, }); diff --git a/src/app/[[...puckPath]]/page.tsx b/src/app/[[...puckPath]]/page.tsx index 34dcb128..4147d98e 100644 --- a/src/app/[[...puckPath]]/page.tsx +++ b/src/app/[[...puckPath]]/page.tsx @@ -14,11 +14,7 @@ export async function generateMetadata({ }; } -export default async function Page({ - params, -}: { - params: Promise<{ puckPath?: string[] }>; -}) { +export default async function Page({ params }: { params: Promise<{ puckPath?: string[] }> }) { const data = await getPublishedDocument(params); if (!data) { diff --git a/src/app/editor/DocumentList.tsx b/src/app/editor/DocumentList.tsx index c959439b..8f67312e 100644 --- a/src/app/editor/DocumentList.tsx +++ b/src/app/editor/DocumentList.tsx @@ -18,13 +18,13 @@ import { ResourceCard, NewResourceCard } from "./ResourceCard"; import { formatRelativeTime } from "@/lib/format"; import { Button } from "@/components/ui/button"; import { useDialogs } from "@/components/ui/dialog-provider"; -import { - Collapsible, - CollapsibleTrigger, - CollapsibleContent, -} from "@/components/ui/collapsible"; +import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; -type DocumentItem = { id: number; name: string | null; lastModified: Date | null }; +type DocumentItem = { + id: number; + name: string | null; + lastModified: Date | null; +}; function DocumentCard({ id, @@ -56,7 +56,11 @@ function useDocumentActions() { const { prompt, alert, confirm } = useDialogs(); async function handleRename(id: number, currentName: string) { - const newName = await prompt({ title: `Rename "${currentName}"`, label: "New name", defaultValue: currentName }); + const newName = await prompt({ + title: `Rename "${currentName}"`, + label: "New name", + defaultValue: currentName, + }); if (newName === null || newName.trim() === "" || newName.trim() === currentName) return; startTransition(async () => { const result = await runAction(renameDocumentAction({ id, name: newName.trim() })); @@ -69,7 +73,10 @@ function useDocumentActions() { } async function handleArchive(id: number, displayName: string) { - const confirmed = await confirm({ message: `Archive "${displayName}"?`, actionLabel: "Archive" }); + const confirmed = await confirm({ + message: `Archive "${displayName}"?`, + actionLabel: "Archive", + }); if (!confirmed) return; startTransition(async () => { const result = await runAction(archiveDocumentAction({ id })); @@ -82,7 +89,10 @@ function useDocumentActions() { } async function handleUnarchive(id: number, displayName: string) { - const confirmed = await confirm({ message: `Unarchive "${displayName}"?`, actionLabel: "Unarchive" }); + const confirmed = await confirm({ + message: `Unarchive "${displayName}"?`, + actionLabel: "Unarchive", + }); if (!confirmed) return; startTransition(async () => { const result = await runAction(unarchiveDocumentAction({ id })); @@ -111,7 +121,13 @@ function useDocumentActions() { }); } - return { isPending, handleRename, handleArchive, handleUnarchive, handleDuplicate }; + return { + isPending, + handleRename, + handleArchive, + handleUnarchive, + handleDuplicate, + }; } function NewDocumentCard() { @@ -235,12 +251,8 @@ export function ArchivedDocumentList({ documents }: { documents: DocumentItem[]

Archived

- } - > - + }> +
diff --git a/src/app/editor/MediaLibrary.tsx b/src/app/editor/MediaLibrary.tsx index 4c14ba7c..a20eea15 100644 --- a/src/app/editor/MediaLibrary.tsx +++ b/src/app/editor/MediaLibrary.tsx @@ -60,11 +60,7 @@ function MediaCard({ + {file.name} ) : ( ) @@ -75,13 +71,34 @@ function MediaCard({ external actions={ <> - - - @@ -109,12 +126,18 @@ export function MediaLibrary({ files: initialFiles }: { files: MediaWithUrl[] }) } async function handleRename(file: MediaWithUrl) { - const newName = await prompt({ title: `Rename "${file.name}"`, label: "New name", defaultValue: file.name }); + const newName = await prompt({ + title: `Rename "${file.name}"`, + label: "New name", + defaultValue: file.name, + }); if (newName === null || newName.trim() === "" || newName.trim() === file.name) return; startTransition(async () => { const result = await runAction(renameMediaAction({ id: file.id, name: newName.trim() })); if (result.success) { - setFiles((prev) => prev.map((f) => (f.id === file.id ? { ...f, name: newName.trim() } : f))); + setFiles((prev) => + prev.map((f) => (f.id === file.id ? { ...f, name: newName.trim() } : f)), + ); } else { await alert(result.error); } @@ -122,7 +145,14 @@ export function MediaLibrary({ files: initialFiles }: { files: MediaWithUrl[] }) } async function handleDelete(file: MediaWithUrl) { - if (!await confirm({ message: `Delete "${file.name}"?`, actionLabel: "Delete", destructive: true })) return; + if ( + !(await confirm({ + message: `Delete "${file.name}"?`, + actionLabel: "Delete", + destructive: true, + })) + ) + return; startTransition(async () => { const result = await runAction(deleteMediaAction({ id: file.id })); if (result.success) { diff --git a/src/app/editor/ResourceCard.tsx b/src/app/editor/ResourceCard.tsx index 444fac99..d4dd9024 100644 --- a/src/app/editor/ResourceCard.tsx +++ b/src/app/editor/ResourceCard.tsx @@ -18,14 +18,10 @@ export function ResourceCard({ external?: boolean; actions?: ReactNode; }) { - const linkProps = external - ? { target: "_blank" as const, rel: "noopener noreferrer" } - : {}; + const linkProps = external ? { target: "_blank" as const, rel: "noopener noreferrer" } : {}; const previewContent = ( -
- {preview} -
+
{preview}
); const nameContent = ( @@ -57,14 +53,10 @@ export function ResourceCard({ ) : ( nameContent )} - - {date ?? "\u00A0"} - + {date ?? "\u00A0"} {actions && ( - - {actions} - + {actions} )} @@ -102,9 +94,7 @@ export function NewResourceCard({ >
+ - - {disabled ? loadingLabel : label} - + {disabled ? loadingLabel : label} {children}
diff --git a/src/app/editor/RouteTable.tsx b/src/app/editor/RouteTable.tsx index 433d1346..4f34829b 100644 --- a/src/app/editor/RouteTable.tsx +++ b/src/app/editor/RouteTable.tsx @@ -3,11 +3,7 @@ import Link from "next/link"; import { useRef, useState, useTransition } from "react"; import { Check, SquarePen, Trash2, X } from "lucide-react"; -import { - createRouteAction, - updateRouteAction, - deleteRouteAction, -} from "../../lib/routes/actions"; +import { createRouteAction, updateRouteAction, deleteRouteAction } from "../../lib/routes/actions"; import { getEditorUrl } from "../../lib/editor-url"; import { runAction } from "./runAction"; import { useDialogs } from "@/components/ui/dialog-provider"; @@ -194,7 +190,10 @@ export function RouteTable({ const result = await runAction(createRouteAction({ path, documentId })); if (result.success) { const doc = documents.find((d) => d.id === documentId)!; - setRoutes((prev) => [...prev, { id: result.data.routeId, path, documentId, documentName: doc.name }]); + setRoutes((prev) => [ + ...prev, + { id: result.data.routeId, path, documentId, documentName: doc.name }, + ]); } else { await alert(result.error); } @@ -206,7 +205,9 @@ export function RouteTable({ const result = await runAction(updateRouteAction({ id, path, documentId })); if (result.success) { const doc = documents.find((d) => d.id === documentId)!; - setRoutes((prev) => prev.map((r) => (r.id === id ? { ...r, path, documentId, documentName: doc.name } : r))); + setRoutes((prev) => + prev.map((r) => (r.id === id ? { ...r, path, documentId, documentName: doc.name } : r)), + ); } else { await alert(result.error); } @@ -214,7 +215,14 @@ export function RouteTable({ } async function handleDelete(route: RouteRow) { - if (!await confirm({ message: `Delete route "${route.path}"?`, actionLabel: "Delete", destructive: true })) return; + if ( + !(await confirm({ + message: `Delete route "${route.path}"?`, + actionLabel: "Delete", + destructive: true, + })) + ) + return; startTransition(async () => { const result = await runAction(deleteRouteAction({ id: route.id })); if (result.success) { diff --git a/src/app/editor/[id]/[slug]/ActionBarOverride.tsx b/src/app/editor/[id]/[slug]/ActionBarOverride.tsx index f0373c4d..a737d928 100644 --- a/src/app/editor/[id]/[slug]/ActionBarOverride.tsx +++ b/src/app/editor/[id]/[slug]/ActionBarOverride.tsx @@ -17,20 +17,13 @@ type ClipboardPayload = { }; /** Check whether a prop is a slot by looking up its field definition in the config. */ -function isSlotArray( - propKey: string, - componentType: string, - config: Config -): boolean { +function isSlotArray(propKey: string, componentType: string, config: Config): boolean { const fieldDef = config.components?.[componentType]?.fields?.[propKey]; return fieldDef?.type === "slot"; } /** Recursively regenerate all `props.id` fields to avoid duplicates. */ -function regenerateIds( - component: ComponentData, - config: Config -): ComponentData { +function regenerateIds(component: ComponentData, config: Config): ComponentData { const clone = structuredClone(component); clone.props.id = crypto.randomUUID(); @@ -38,9 +31,7 @@ function regenerateIds( if (key === "id") continue; const val = clone.props[key]; if (Array.isArray(val) && isSlotArray(key, clone.type, config)) { - clone.props[key] = val.map((child: ComponentData) => - regenerateIds(child, config) - ); + clone.props[key] = val.map((child: ComponentData) => regenerateIds(child, config)); } } @@ -143,9 +134,7 @@ export function ActionBarOverride({ // Validate component type exists in config // (in case a user copies components from different versions, for example) if (!config.components?.[component.type]) { - console.warn( - `Cannot paste: unknown component type "${component.type}"` - ); + console.warn(`Cannot paste: unknown component type "${component.type}"`); return; } diff --git a/src/app/editor/[id]/[slug]/EditorPage.tsx b/src/app/editor/[id]/[slug]/EditorPage.tsx index 699b84ab..41ed4510 100644 --- a/src/app/editor/[id]/[slug]/EditorPage.tsx +++ b/src/app/editor/[id]/[slug]/EditorPage.tsx @@ -28,7 +28,7 @@ export default async function EditorPage({ } const data = targetVersionId - ? (await getVersionContent(targetVersionId)) ?? createEmptyPuckData() + ? ((await getVersionContent(targetVersionId)) ?? createEmptyPuckData()) : createEmptyPuckData(); const versions = document.versions.map((v) => ({ diff --git a/src/app/editor/[id]/[slug]/VersionListPanel.tsx b/src/app/editor/[id]/[slug]/VersionListPanel.tsx index 73a35b15..8dc02cd7 100644 --- a/src/app/editor/[id]/[slug]/VersionListPanel.tsx +++ b/src/app/editor/[id]/[slug]/VersionListPanel.tsx @@ -18,8 +18,14 @@ export interface VersionListPanelProps { function formatVersionLabel(version: Version) { const date = new Date(version.createdAt); const relative = formatRelativeTime(date); - const month = date.toLocaleDateString("en-US", { month: "long", day: "numeric" }); - const time = date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).toLowerCase().replace(" ", ""); + const month = date.toLocaleDateString("en-US", { + month: "long", + day: "numeric", + }); + const time = date + .toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }) + .toLowerCase() + .replace(" ", ""); const absolute = `${month}, ${time}`; return { relative, absolute }; } @@ -50,25 +56,19 @@ function VersionEntry({ onClick={onLoad} className={cn( "px-3 py-2 rounded text-xs cursor-pointer transition", - isCurrent - ? "bg-gray-100 font-semibold" - : "bg-white hover:bg-gray-50", + isCurrent ? "bg-gray-100 font-semibold" : "bg-white hover:bg-gray-50", )} >
{absolute} -
- {relative} -
+
{relative}
{isPublished ? ( - - Published - + Published ) : (