diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml index cdf466d..3e24e7f 100644 --- a/.github/workflows/migrate.yml +++ b/.github/workflows/migrate.yml @@ -29,3 +29,11 @@ jobs: DATABASE_URL: ${{ secrets.DATABASE_URL }} DIRECT_URL: ${{ secrets.DIRECT_URL }} run: npx prisma migrate deploy + + - name: Seed database + if: github.ref == 'refs/heads/dev' + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + DIRECT_URL: ${{ secrets.DIRECT_URL }} + NODE_ENV: development + run: npm run prisma:seed diff --git a/package-lock.json b/package-lock.json index 01c9e6f..482bd9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "postcss": "^8.5.6", "prettier": "^3.8.1", "tailwindcss": "^4.1.16", + "tsx": "^4.21.0", "typescript": "^5.9.3" } }, @@ -90,6 +91,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -359,6 +361,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -2157,6 +2601,7 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.95.3.tgz", "integrity": "sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg==", "license": "MIT", + "peer": true, "dependencies": { "@supabase/auth-js": "2.95.3", "@supabase/functions-js": "2.95.3", @@ -2550,6 +2995,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2560,6 +3006,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2631,6 +3078,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -3148,6 +3596,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3500,6 +3949,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -4225,6 +4675,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4253,6 +4745,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4438,6 +4931,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4878,6 +5372,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6701,6 +7210,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6778,6 +7288,7 @@ "integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.18.0", "@prisma/engines": "6.18.0" @@ -6876,6 +7387,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6885,6 +7397,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6897,6 +7410,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7586,7 +8100,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -7691,6 +8206,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7788,6 +8323,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8125,6 +8661,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index a49b4ab..926eaba 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,16 @@ "lint": "next lint", "db:start": "docker compose up -d", "db:stop": "docker compose down", - "db:reset": "docker compose down -v && docker compose up -d --wait && npm run prisma:migrate", + "db:reset": "docker compose down -v && docker compose up -d --wait && npm run prisma:migrate && npm run prisma:seed", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", + "prisma:seed": "prisma db seed", "prisma:studio": "prisma studio", "postinstall": "prisma generate" }, + "prisma": { + "seed": "tsx --tsconfig tsconfig.json prisma/seed.ts" + }, "private": true, "dependencies": { "@hookform/resolvers": "^5.2.2", @@ -51,6 +55,7 @@ "postcss": "^8.5.6", "prettier": "^3.8.1", "tailwindcss": "^4.1.16", + "tsx": "^4.21.0", "typescript": "^5.9.3" } } diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..a2efb4e --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,25 @@ +import { seedCommunityConstituencies } from '@/prisma/seed/community-constituencies' +import { seedSettings } from '@/prisma/seed/settings' +import { seedApplications } from '@/prisma/seed/applications' +import { seedNominations } from '@/prisma/seed/nominations' +import { seedEndorsements } from '@/prisma/seed/endorsements' +import { db } from '@/lib/db' + +async function main() { + const nodeEnv = process.env.NODE_ENV || 'development' + + if (nodeEnv === 'production') { + return + } + + const existingData = await db.communityConstituency.findFirst() + if (existingData) return + + const constituencies = await seedCommunityConstituencies() + await seedSettings() + await seedApplications(constituencies) + await seedNominations(constituencies) + await seedEndorsements() +} + +main() diff --git a/prisma/seed/applications.ts b/prisma/seed/applications.ts new file mode 100644 index 0000000..d395f9d --- /dev/null +++ b/prisma/seed/applications.ts @@ -0,0 +1,57 @@ +import { CommunityConstituency } from '@prisma/client' +import { db } from '@/lib/db' +import { + FIRST_NAMES, + LAST_NAMES, + COLLEGES, + MAJORS, + MINORS, + YEARS, + SEMESTERS, + CONSTITUENCIES, + PRONOUNS, + LONG_ANSWERS, + randomItem, + generateNUID, + generateEmail, + generatePhoneNumber, +} from '@/prisma/seed/constants' + +export async function seedApplications(constituencies: CommunityConstituency[]) { + const applications = [] + + for (let i = 0; i < 5; i++) { + const firstName = randomItem(FIRST_NAMES) + const lastName = randomItem(LAST_NAMES) + const fullName = `${firstName} ${lastName}` + + applications.push({ + nuid: generateNUID(), + fullName, + preferredFullName: fullName, + phoneticPronunciation: fullName, + pronunciationAudioUrl: `https://example.com/audio/${i}.mp3`, + pronouns: randomItem(PRONOUNS), + email: generateEmail(firstName, lastName), + phoneNumber: generatePhoneNumber(), + college: randomItem(COLLEGES), + major: randomItem(MAJORS), + minors: randomItem(MINORS), + year: randomItem(YEARS), + semester: randomItem(SEMESTERS), + constituency: randomItem(CONSTITUENCIES), + communityConstituencyId: i < 3 ? constituencies[i].id : null, + whySenateLongAnswer: randomItem(LONG_ANSWERS.whySenate), + constituencyIssueLongAnswer: randomItem(LONG_ANSWERS.constituencyIssue), + diversityEquityInclusionLongAnswer: randomItem(LONG_ANSWERS.diversityEquityInclusion), + conflictSituationLongAnswer: randomItem(LONG_ANSWERS.conflictSituation), + campaignBlurb: `Vote ${firstName} ${lastName} for Student Senate - Committed to representing your voice!`, + nominationFormPdfUrl: `https://example.com/nominations/${i}.pdf`, + }) + } + + await db.application.createMany({ + data: applications, + skipDuplicates: true, + }) +} diff --git a/prisma/seed/community-constituencies.ts b/prisma/seed/community-constituencies.ts new file mode 100644 index 0000000..ad748f7 --- /dev/null +++ b/prisma/seed/community-constituencies.ts @@ -0,0 +1,11 @@ +import { db } from '@/lib/db' +import { COMMUNITY_CONSTITUENCIES } from '@/prisma/seed/constants' + +export async function seedCommunityConstituencies() { + await db.communityConstituency.createMany({ + data: COMMUNITY_CONSTITUENCIES, + skipDuplicates: true, + }) + + return db.communityConstituency.findMany() +} diff --git a/prisma/seed/constants.ts b/prisma/seed/constants.ts new file mode 100644 index 0000000..8809c3b --- /dev/null +++ b/prisma/seed/constants.ts @@ -0,0 +1,111 @@ +export const FIRST_NAMES = ['Alex', 'Jordan', 'Taylor', 'Morgan', 'Casey'] +export const LAST_NAMES = ['Smith', 'Johnson', 'Williams', 'Brown', 'Davis'] +export const COLLEGES = [ + 'Khoury College of Computer Sciences', + 'College of Engineering', + 'College of Science', + 'D\'Amore-McKim School of Business', + 'College of Social Sciences and Humanities' +] +export const MAJORS = ['Computer Science', 'Data Science', 'Business Administration', 'Political Science', 'Biology'] +export const MINORS = ['Mathematics', 'Economics', 'Psychology', '', 'Philosophy'] +export const YEARS = [1, 2, 3, 4, 5] +export const SEMESTERS = ['Fall 2024', 'Spring 2025', 'Fall 2025', 'Spring 2026', 'Fall 2026'] +export const CONSTITUENCIES = ['Khoury', 'Engineering', 'Science', 'Business', 'CSSH'] +export const PRONOUNS = ['he/him', 'she/her', 'they/them', 'she/they', 'he/they'] + +export const COMMUNITY_CONSTITUENCIES = [ + { name: 'International Students', isActive: true }, + { name: 'First Generation Students', isActive: true }, + { name: 'Transfer Students', isActive: true }, + { name: 'Veterans and Military Affiliated', isActive: true }, + { name: 'LGBTQ+ Community', isActive: false } +] + +export const LONG_ANSWERS = { + whySenate: [ + 'I am passionate about representing students and making a positive impact on campus. I believe my experience and dedication make me a strong candidate for student senate.', + 'Student government has always been important to me as a way to create meaningful change. I want to ensure every voice is heard in decision-making processes.', + 'My goal is to bridge the gap between students and administration, advocating for policies that improve campus life and academic experiences.', + 'I have a track record of leadership and want to use those skills to serve the student body and address pressing issues facing our community.', + 'Being part of student senate would allow me to work on initiatives I care about while representing the diverse perspectives of my fellow students.' + ], + constituencyIssue: [ + 'The main issues include access to resources, representation in decision-making, and building community. I plan to address these by working closely with administration and advocating for student needs.', + 'Students face challenges with academic support, mental health resources, and campus engagement. I will work to expand services and create more inclusive programs.', + 'Key concerns are affordability, career preparation, and campus safety. I will collaborate with stakeholders to develop comprehensive solutions.', + 'We need better communication between students and faculty, improved facilities, and more opportunities for student involvement in governance.', + 'The biggest issues are accessibility, equity in resource distribution, and creating a welcoming environment for all students regardless of background.' + ], + diversityEquityInclusion: [ + 'I am committed to creating an inclusive environment where all students feel valued and heard. I will work to ensure diverse perspectives are represented in all discussions and decisions.', + 'Diversity strengthens our community. I will advocate for programs that celebrate different cultures and create spaces for meaningful dialogue across differences.', + 'Equity means ensuring everyone has access to the resources they need. I will push for policies that address systemic barriers and promote equal opportunity.', + 'Inclusion requires active effort. I will work to amplify marginalized voices and ensure decision-making processes reflect the full diversity of our student body.', + 'I believe in proactive measures to create belonging, from inclusive events to equitable policies that support students from all backgrounds.' + ], + conflictSituation: [ + 'In conflict situations, I believe in listening to all parties, finding common ground, and working collaboratively toward solutions that benefit everyone involved.', + 'I approach conflicts with empathy and patience, seeking to understand different perspectives before proposing resolutions that address root causes.', + 'My strategy is to facilitate open dialogue, mediate disputes fairly, and find creative compromises that respect everyone\'s needs and concerns.', + 'I handle conflicts by staying calm, gathering all relevant information, and working with stakeholders to develop mutually beneficial solutions.', + 'Conflict resolution requires active listening, clear communication, and a commitment to fairness. I focus on building consensus and maintaining relationships.' + ] +} + +export const ENDORSEMENT_CONTENT = { + definingTraits: [ + 'Dedicated, passionate, and committed to student advocacy. Shows strong leadership potential and genuine care for the student body.', + 'Innovative thinker with excellent interpersonal skills. Demonstrates reliability, integrity, and a collaborative approach to problem-solving.', + 'Empathetic listener who builds consensus. Shows resilience, creativity, and dedication to making positive change on campus.', + 'Natural leader with strong communication abilities. Displays initiative, responsibility, and genuine commitment to serving others.', + 'Strategic and thoughtful with a proven track record. Combines vision with practical action and inspires others through example.' + ], + leadershipQualities: [ + 'Excellent communication skills, ability to work collaboratively, strategic thinking, and proven track record of initiative.', + 'Strong organizational abilities, empowers team members, thinks critically under pressure, and maintains ethical standards.', + 'Builds inclusive teams, delegates effectively, stays focused on goals, and adapts well to changing circumstances.', + 'Inspires trust and confidence, makes thoughtful decisions, takes accountability, and demonstrates emotional intelligence.', + 'Motivates others toward shared vision, balances multiple priorities, communicates clearly, and leads by example.' + ], + areasForDevelopment: [ + 'Could benefit from more experience in formal governance processes and public speaking in large settings.', + 'Would grow from additional exposure to budget management and long-term strategic planning.', + 'Could develop skills in conflict mediation and navigating complex organizational structures.', + 'Would benefit from experience managing larger teams and coordinating multi-stakeholder initiatives.', + 'Could strengthen abilities in data analysis and evidence-based policy development.' + ] +} + +export const randomItem = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)] + +export const randomInt = (min: number, max: number): number => + Math.floor(Math.random() * (max - min + 1)) + min + +export const generateNUID = (): string => { + const randomDigits = randomInt(100000, 999999) + return `001${randomDigits}` +} + +export const generateEmail = (firstName: string, lastName: string): string => { + const randomNum = randomInt(1, 9999) + const cleanFirst = firstName.toLowerCase().replace(/[^a-z]/g, '') + const cleanLast = lastName.toLowerCase().replace(/[^a-z]/g, '') + return `${cleanFirst}.${cleanLast}${randomNum}@northeastern.edu` +} + +export const generatePhoneNumber = (): string => { + const areaCode = randomInt(200, 999) + const prefix = randomInt(200, 999) + const lineNumber = randomInt(1000, 9999) + return `${areaCode}-${prefix}-${lineNumber}` +} + +export const getRandomConstituencyId = ( + index: number, + constituencies: Array<{ id: string }> +): string | null => { + if (index % 2 === 0) return null + if (index >= 4) return null + return constituencies[Math.floor(index / 2)].id +} diff --git a/prisma/seed/endorsements.ts b/prisma/seed/endorsements.ts new file mode 100644 index 0000000..333920d --- /dev/null +++ b/prisma/seed/endorsements.ts @@ -0,0 +1,33 @@ +import { db } from '@/lib/db' +import { + FIRST_NAMES, + LAST_NAMES, + ENDORSEMENT_CONTENT, + randomItem, + generateEmail, +} from '@/prisma/seed/constants' + +export async function seedEndorsements() { + const endorsements = [] + + for (let i = 0; i < 5; i++) { + const endorserFirst = randomItem(FIRST_NAMES) + const endorserLast = randomItem(LAST_NAMES) + const applicantFirst = randomItem(FIRST_NAMES) + const applicantLast = randomItem(LAST_NAMES) + + endorsements.push({ + endorserName: `${endorserFirst} ${endorserLast}`, + endorserEmail: generateEmail(endorserFirst, endorserLast), + applicantName: `${applicantFirst} ${applicantLast}`, + definingTraits: randomItem(ENDORSEMENT_CONTENT.definingTraits), + leadershipQualities: randomItem(ENDORSEMENT_CONTENT.leadershipQualities), + areasForDevelopment: randomItem(ENDORSEMENT_CONTENT.areasForDevelopment), + }) + } + + await db.endorsement.createMany({ + data: endorsements, + skipDuplicates: true, + }) +} diff --git a/prisma/seed/nominations.ts b/prisma/seed/nominations.ts new file mode 100644 index 0000000..1aae531 --- /dev/null +++ b/prisma/seed/nominations.ts @@ -0,0 +1,40 @@ +import { CommunityConstituency } from '@prisma/client' +import { db } from '@/lib/db' +import { + FIRST_NAMES, + LAST_NAMES, + COLLEGES, + MAJORS, + randomItem, + generateEmail, + getRandomConstituencyId, +} from '@/prisma/seed/constants' + +const STATUSES = ['PENDING', 'APPROVED', 'REJECTED', 'PENDING', 'APPROVED'] as const + +export async function seedNominations(constituencies: CommunityConstituency[]) { + const nominations = [] + + for (let i = 0; i < 5; i++) { + const firstName = randomItem(FIRST_NAMES) + const lastName = randomItem(LAST_NAMES) + const nominatorFirst = randomItem(FIRST_NAMES) + const nominatorLast = randomItem(LAST_NAMES) + + nominations.push({ + nominee: `${firstName} ${lastName}`, + fullName: `${nominatorFirst} ${nominatorLast}`, + email: generateEmail(nominatorFirst, nominatorLast), + college: randomItem(COLLEGES), + major: randomItem(MAJORS), + status: STATUSES[i], + constituencyType: i % 2 === 0 ? 'academic' : 'community', + communityConstituencyId: getRandomConstituencyId(i, constituencies), + }) + } + + await db.nomination.createMany({ + data: nominations, + skipDuplicates: true, + }) +} diff --git a/prisma/seed/settings.ts b/prisma/seed/settings.ts new file mode 100644 index 0000000..5340b85 --- /dev/null +++ b/prisma/seed/settings.ts @@ -0,0 +1,16 @@ +import { db } from '@/lib/db' + +export async function seedSettings() { + await db.settings.create({ + data: { + requiredNominations: 15, + maxCommunityNominations: 7, + endorsementRequired: false, + endorsementsOpen: true, + applicationDeadline: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + applicationsOpen: true, + nominationsOpen: true, + customMessage: 'Welcome to the Student Senate Application System!', + }, + }) +}