diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..accf79e327469c30c9457a0b38cc1c9df1046c48 Binary files /dev/null and b/.DS_Store differ diff --git a/movie-group-8/.env b/movie-group-8/.env new file mode 100644 index 0000000000000000000000000000000000000000..4ec34fde83f0116c4ff586e6f695eb0a151ad951 --- /dev/null +++ b/movie-group-8/.env @@ -0,0 +1,3 @@ +VITE_TMDB_API_KEY=42259df77843511296d8096fa29e08a8 + +VITE_TMDB_BASE=https://api.themoviedb.org/3 diff --git a/movie-group-8/.gitignore b/movie-group-8/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a547bf36d8d11a4f89c59c144f24795749086dd1 --- /dev/null +++ b/movie-group-8/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/movie-group-8/.vscode/extensions.json b/movie-group-8/.vscode/extensions.json new file mode 100644 index 0000000000000000000000000000000000000000..a7cea0b0678120a1b590d1b6592c7318039b9179 --- /dev/null +++ b/movie-group-8/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/movie-group-8/README.md b/movie-group-8/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1511959c22b74118c06679739cf9abe2ceeea48c --- /dev/null +++ b/movie-group-8/README.md @@ -0,0 +1,5 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. + +Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support). diff --git a/movie-group-8/index.html b/movie-group-8/index.html new file mode 100644 index 0000000000000000000000000000000000000000..e693fa6a9079d02a1f3a87b4e6cae1e788a11a21 --- /dev/null +++ b/movie-group-8/index.html @@ -0,0 +1,13 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/png" href="./src/assets/Dark_Mode.png" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>SceneIt</title> + </head> + <body> + <div id="app"></div> + <script type="module" src="/src/main.js"></script> + </body> +</html> diff --git a/movie-group-8/package-lock.json b/movie-group-8/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..5502da0ec210ec42ef3ac199421a2d8977f55e32 --- /dev/null +++ b/movie-group-8/package-lock.json @@ -0,0 +1,2877 @@ +{ + "name": "movie-group-8", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "movie-group-8", + "version": "0.0.0", + "dependencies": { + "@headlessui/vue": "^1.7.23", + "@heroicons/vue": "^2.2.0", + "@tailwindcss/vite": "^4.0.14", + "axios": "^1.9.0", + "firebase": "^11.4.0", + "movie-group-8": "file:", + "tailwindcss": "^4.0.14", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "dependencies": { + "@babel/types": "^7.26.10" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.12", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.12.tgz", + "integrity": "sha512-iDCGnw6qdFqwI5ywkgece99WADJNoymu+nLIQI4fZM/vCZ3bEo4wlpEetW71s1HqGpI0hQStiPhqVjFxDb2yyw==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.18.tgz", + "integrity": "sha512-Hw9mzsSMZaQu6wrTbi3kYYwGw9nBqOHr47pVLxfr5v8CalsdrG5gfs9XUlPOZjHRVISp3oQrh1j7d3E+ulHPjQ==", + "dependencies": { + "@firebase/analytics": "0.10.12", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.6.13", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==" + }, + "node_modules/@firebase/app": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.11.2.tgz", + "integrity": "sha512-bFee0hPJZBzNtiizRxdgsu8C9DW3mn1y0OJJ4zHQsccjDYzGOfvN0G3CMGyBIiwNctsFpQa8orbp2IKywoUeqA==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.12.tgz", + "integrity": "sha512-LxjcoIFOU4sgK07ZWb8XDHxuVB+UKs41vPK+Sg9PeZMvEoz84fndFAx8Nz2nipiya2EmyxBgVhff8Hi6GBt+XA==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.19.tgz", + "integrity": "sha512-G8FMiqhrKc4gEEujrBDBBrbRav8MGqoLObWj1hy/riCSg4XlRYhpnq3ev8E9HTirqU1tAGH6oJl7vr+jfM7YNA==", + "dependencies": { + "@firebase/app-check": "0.8.12", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.51", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.51.tgz", + "integrity": "sha512-pxF1+coABt+ugqNI0YXDlmkKv4kh3pjI5BqIJJ1VXBo42OZbKMsQbFeos14YBrWwiqqSjUvQ70FBNsv5E2wuxg==", + "dependencies": { + "@firebase/app": "0.11.2", + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==" + }, + "node_modules/@firebase/auth": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.9.1.tgz", + "integrity": "sha512-9KKo5SNVkyJzftsW+daS+PGDbeJ+MFJWXQFHDqqPPH3acWHtiNnGHH5HGpIJErEELrsm9xMPie5zfZ0XpGU8+w==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.19.tgz", + "integrity": "sha512-v898POphOIBJliKF76SiGOXh4EdhO5fM6S9a2ZKf/8wHdBea/qwxwZoVVya4DW6Mi7vWyp1lIzHbFgwRz8G9TA==", + "dependencies": { + "@firebase/auth": "1.9.1", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.6.13", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.13.tgz", + "integrity": "sha512-I/Eg1NpAtZ8AAfq8mpdfXnuUpcLxIDdCDtTzWSh+FXnp/9eCKJ3SNbOCKrUCyhLzNa2SiPJYruei0sxVjaOTeg==", + "dependencies": { + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.1.tgz", + "integrity": "sha512-PNlfAJ2mcbyRlWfm41nfk8EksTuvMFTFIX+puNzeUa6OTIDtyp1IX1NJVc7n6WpfbErN7tNqcOEMe6BMtpcjVA==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.13.tgz", + "integrity": "sha512-cdc+LuseKdJXzlrCx8ePMXyctSWtYS9SsP3y7EeA85GzNh/IL0b7HOq0eShridL935iQ0KScZCj5qJtKkGE53g==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.4.tgz", + "integrity": "sha512-4qsptwZ3DTGNBje56ETItZQyA/HMalOelnLmkC3eR0M6+zkzOHjNHyWUWodW2mqxRKAM0sGkn+aIwYHKZFJXug==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/database": "1.0.13", + "@firebase/database-types": "1.0.9", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.9.tgz", + "integrity": "sha512-uCntrxPbJHhZsNRpMhxNCm7GzhYWX+7J2e57wq1ZZ4NJrQw5DORgkAzJMByYZcVAjgADnCxxhK/GkoypH+XpvQ==", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.11.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.9.tgz", + "integrity": "sha512-uq/bUtHDqJ5ZqPHAJIlNzHpXUtcVYcASz2V6y7UmP1WLlRKEt1yf1OcQW5u8pY2yq7162OnCl5J5mkOdMTMLZw==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "@firebase/webchannel-wrapper": "1.0.3", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.44", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.44.tgz", + "integrity": "sha512-4Lv2TyHEW+FugXPgmQ0ZylSbh9uFuKDP0lCL1hX9cbxXaafhC/Nww+DWokUQ2zZcynjc8fxFunw6Xbd3QHAlgA==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/firestore": "4.7.9", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.3.tgz", + "integrity": "sha512-Wv7JZMUkKLb1goOWRtsu3t7m97uK6XQvjQLPvn8rncY91+VgdU72crqnaYCDI/ophNuBEmuK8mn0/pAnjUeA6A==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.13", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.20.tgz", + "integrity": "sha512-iIudmYDAML6n3c7uXO2YTlzra2/J6lnMzmJTXNthvrKVMgNMaseNoQP1wKfchK84hMuSF8EkM4AvufwbJ+Juew==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/functions": "0.12.3", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==" + }, + "node_modules/@firebase/installations": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.13.tgz", + "integrity": "sha512-6ZpkUiaygPFwgVneYxuuOuHnSPnTA4KefLEaw/sKk/rNYgC7X6twaGfYb0sYLpbi9xV4i5jXsqZ3WO+yaguNgg==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/util": "1.11.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.13.tgz", + "integrity": "sha512-f/o6MqCI7LD/ulY9gvgkv6w5k6diaReD8BFHd/y/fEdpsXmFWYS/g28GXCB72bRVBOgPpkOUNl+VsMvDwlRKmw==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", + "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.17", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.17.tgz", + "integrity": "sha512-W3CnGhTm6Nx8XGb6E5/+jZTuxX/EK8Vur4QXvO1DwZta/t0xqWMRgO9vNsZFMYBqFV4o3j4F9qK/iddGYwWS6g==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.11.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.17.tgz", + "integrity": "sha512-5Q+9IG7FuedusdWHVQRjpA3OVD9KUWp/IPegcv0s5qSqRLBjib7FlAeWxN+VL0Ew43tuPJBY2HKhEecuizmO1Q==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/messaging": "0.12.17", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==" + }, + "node_modules/@firebase/performance": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.1.tgz", + "integrity": "sha512-SkEUurawojCjav2V2AXo6BQLDtv02NxgXPLCiAvrkn95IAKI4W/UbLKYQvMbEez/nqvmnucLyklcMlB0Q5a1iw==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.14.tgz", + "integrity": "sha512-/crPg0fDqHIx+FjFoEqWxNp+lJSF40ZG7x43AAJGRaUaWLJDncQm3UJB5/mABaRZb7obs1CQAcRtd4phZFkmZg==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/performance": "0.7.1", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.0.tgz", + "integrity": "sha512-Yrk4l5+6FJLPHC6irNHMzgTtJ3NfHXlAXVChCBdNFtgmzyGmufNs/sr8oA0auEfIJ5VpXCaThRh3P4OdQxiAlQ==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.13.tgz", + "integrity": "sha512-UmHoO7TxAEJPIZf8e1Hy6CeFGMeyjqSCpgoBkQZYXFI2JHhzxIyDpr8jVKJJN1dmAePKZ5EX7dC13CmcdTOl7Q==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/remote-config": "0.6.0", + "@firebase/remote-config-types": "0.4.0", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", + "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==" + }, + "node_modules/@firebase/storage": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.7.tgz", + "integrity": "sha512-FkRyc24rK+Y6EaQ1tYFm3TevBnnfSNA0VyTfew2hrYyL/aYfatBg7HOgktUdB4kWMHNA9VoTotzZTGoLuK92wg==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.17.tgz", + "integrity": "sha512-CBlODWEZ5b6MJWVh21VZioxwxNwVfPA9CAdsk+ZgVocJQQbE2oDW1XJoRcgthRY1HOitgbn4cVrM+NlQtuUYhw==", + "dependencies": { + "@firebase/component": "0.6.13", + "@firebase/storage": "0.13.7", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.0.tgz", + "integrity": "sha512-PzSrhIr++KI6y4P6C/IdgBNMkEx0Ex6554/cYd0Hm+ovyFSJtJXqb/3OSIdnBoa2cpwZT1/GW56EmRc5qEc5fQ==", + "hasInstallScript": true, + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/vertexai": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/vertexai/-/vertexai-1.1.0.tgz", + "integrity": "sha512-K8CgIFKJrfrf5lYhKnDXOu08FEmIzVExK+ApUZx4Bw2GAmLEA3wDVrsjuupuvpXZSp8QlzvEiXwqshqqc4v0pA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.6.13", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.11.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz", + "integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@headlessui/vue": { + "version": "1.7.23", + "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz", + "integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==", + "dependencies": { + "@tanstack/vue-virtual": "^3.0.0-beta.60" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@heroicons/vue": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz", + "integrity": "sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==", + "peerDependencies": { + "vue": ">= 3" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz", + "integrity": "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.36.0.tgz", + "integrity": "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.36.0.tgz", + "integrity": "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.36.0.tgz", + "integrity": "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.36.0.tgz", + "integrity": "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.36.0.tgz", + "integrity": "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.36.0.tgz", + "integrity": "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.36.0.tgz", + "integrity": "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.36.0.tgz", + "integrity": "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.36.0.tgz", + "integrity": "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.36.0.tgz", + "integrity": "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.36.0.tgz", + "integrity": "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.36.0.tgz", + "integrity": "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.36.0.tgz", + "integrity": "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.36.0.tgz", + "integrity": "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.36.0.tgz", + "integrity": "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.36.0.tgz", + "integrity": "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.36.0.tgz", + "integrity": "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.36.0.tgz", + "integrity": "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.14.tgz", + "integrity": "sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w==", + "dependencies": { + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "tailwindcss": "4.0.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.14.tgz", + "integrity": "sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.0.14", + "@tailwindcss/oxide-darwin-arm64": "4.0.14", + "@tailwindcss/oxide-darwin-x64": "4.0.14", + "@tailwindcss/oxide-freebsd-x64": "4.0.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.14", + "@tailwindcss/oxide-linux-x64-musl": "4.0.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.14.tgz", + "integrity": "sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.14.tgz", + "integrity": "sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.14.tgz", + "integrity": "sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.14.tgz", + "integrity": "sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.14.tgz", + "integrity": "sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.14.tgz", + "integrity": "sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.14.tgz", + "integrity": "sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.14.tgz", + "integrity": "sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.14.tgz", + "integrity": "sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.14.tgz", + "integrity": "sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.14.tgz", + "integrity": "sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.14.tgz", + "integrity": "sha512-y69ztPTRFy+13EPS/7dEFVl7q2Goh1pQueVO8IfGeyqSpcx/joNJXFk0lLhMgUbF0VFJotwRSb9ZY7Xoq3r26Q==", + "dependencies": { + "@tailwindcss/node": "4.0.14", + "@tailwindcss/oxide": "4.0.14", + "lightningcss": "1.29.2", + "tailwindcss": "4.0.14" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.4.tgz", + "integrity": "sha512-fNGO9fjjSLns87tlcto106enQQLycCKR4DPNpgq3djP5IdcPFdPAmaKjsgzIeRhH7hWrELgW12hYnRthS5kLUw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.4.tgz", + "integrity": "sha512-1fPrd3hE1SS4R/9JbX1AlzueY4duCK7ixuLcMW5GMnk9N6WbLo9MioNKiv22V+UaXKOLNy8tLdzT8NYerOFTOQ==", + "dependencies": { + "@tanstack/virtual-core": "3.13.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", + "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/firebase": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.4.0.tgz", + "integrity": "sha512-Z6kwhWIPDgIm0+NUEQxwjH14hMP7t42WSFnf/78R0Vh59VovLYTOCTM3MIdY3jlSZ9uKz56FhXrvsNXNhAn/Xg==", + "dependencies": { + "@firebase/analytics": "0.10.12", + "@firebase/analytics-compat": "0.2.18", + "@firebase/app": "0.11.2", + "@firebase/app-check": "0.8.12", + "@firebase/app-check-compat": "0.3.19", + "@firebase/app-compat": "0.2.51", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.9.1", + "@firebase/auth-compat": "0.5.19", + "@firebase/data-connect": "0.3.1", + "@firebase/database": "1.0.13", + "@firebase/database-compat": "2.0.4", + "@firebase/firestore": "4.7.9", + "@firebase/firestore-compat": "0.3.44", + "@firebase/functions": "0.12.3", + "@firebase/functions-compat": "0.3.20", + "@firebase/installations": "0.6.13", + "@firebase/installations-compat": "0.2.13", + "@firebase/messaging": "0.12.17", + "@firebase/messaging-compat": "0.2.17", + "@firebase/performance": "0.7.1", + "@firebase/performance-compat": "0.2.14", + "@firebase/remote-config": "0.6.0", + "@firebase/remote-config-compat": "0.2.13", + "@firebase/storage": "0.13.7", + "@firebase/storage-compat": "0.3.17", + "@firebase/util": "1.11.0", + "@firebase/vertexai": "1.1.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "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", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/movie-group-8": { + "resolved": "", + "link": true + }, + "node_modules/nanoid": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz", + "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.36.0.tgz", + "integrity": "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.36.0", + "@rollup/rollup-android-arm64": "4.36.0", + "@rollup/rollup-darwin-arm64": "4.36.0", + "@rollup/rollup-darwin-x64": "4.36.0", + "@rollup/rollup-freebsd-arm64": "4.36.0", + "@rollup/rollup-freebsd-x64": "4.36.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.36.0", + "@rollup/rollup-linux-arm-musleabihf": "4.36.0", + "@rollup/rollup-linux-arm64-gnu": "4.36.0", + "@rollup/rollup-linux-arm64-musl": "4.36.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.36.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.36.0", + "@rollup/rollup-linux-riscv64-gnu": "4.36.0", + "@rollup/rollup-linux-s390x-gnu": "4.36.0", + "@rollup/rollup-linux-x64-gnu": "4.36.0", + "@rollup/rollup-linux-x64-musl": "4.36.0", + "@rollup/rollup-win32-arm64-msvc": "4.36.0", + "@rollup/rollup-win32-ia32-msvc": "4.36.0", + "@rollup/rollup-win32-x64-msvc": "4.36.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz", + "integrity": "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", + "integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/movie-group-8/package.json b/movie-group-8/package.json new file mode 100644 index 0000000000000000000000000000000000000000..22af383cd1cebd8a1c2023b2f9e7938c34cbac5d --- /dev/null +++ b/movie-group-8/package.json @@ -0,0 +1,26 @@ +{ + "name": "movie-group-8", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@headlessui/vue": "^1.7.23", + "@heroicons/vue": "^2.2.0", + "@tailwindcss/vite": "^4.0.14", + "axios": "^1.9.0", + "firebase": "^11.4.0", + "movie-group-8": "file:", + "tailwindcss": "^4.0.14", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.2.0" + } +} diff --git a/movie-group-8/public/vite.svg b/movie-group-8/public/vite.svg new file mode 100644 index 0000000000000000000000000000000000000000..e7b8dfb1b2a60bd50538bec9f876511b9cac21e3 --- /dev/null +++ b/movie-group-8/public/vite.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> \ No newline at end of file diff --git a/movie-group-8/src/App.vue b/movie-group-8/src/App.vue new file mode 100644 index 0000000000000000000000000000000000000000..06a35b97d34a73ade52d72d75981b01ddec76847 --- /dev/null +++ b/movie-group-8/src/App.vue @@ -0,0 +1,16 @@ +<script setup> +import { RouterLink, RouterView } from 'vue-router'; +import Navbar from './components/Navbar.vue'; +</script> + +<template> + <Navbar /> + <div class="z-10 bg-neutral-900"> + <RouterView /> + </div> +</template> + +<style scoped> +</style> + + diff --git a/movie-group-8/src/assets/Dark_Mode.png b/movie-group-8/src/assets/Dark_Mode.png new file mode 100644 index 0000000000000000000000000000000000000000..5bdc467c008da01416a4764c34f69c1855c9420b Binary files /dev/null and b/movie-group-8/src/assets/Dark_Mode.png differ diff --git a/movie-group-8/src/assets/Light_Mode.png b/movie-group-8/src/assets/Light_Mode.png new file mode 100644 index 0000000000000000000000000000000000000000..d345b08cf1fcdb1107bda38811905070fa5206d3 Binary files /dev/null and b/movie-group-8/src/assets/Light_Mode.png differ diff --git a/movie-group-8/src/assets/TMDB.svg b/movie-group-8/src/assets/TMDB.svg new file mode 100644 index 0000000000000000000000000000000000000000..42f31f15442e3afe6ae8b38024637112e508bf7a --- /dev/null +++ b/movie-group-8/src/assets/TMDB.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 190.24 81.52"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 2</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M105.67,36.06h66.9A17.67,17.67,0,0,0,190.24,18.4h0A17.67,17.67,0,0,0,172.57.73h-66.9A17.67,17.67,0,0,0,88,18.4h0A17.67,17.67,0,0,0,105.67,36.06Zm-88,45h76.9A17.67,17.67,0,0,0,112.24,63.4h0A17.67,17.67,0,0,0,94.57,45.73H17.67A17.67,17.67,0,0,0,0,63.4H0A17.67,17.67,0,0,0,17.67,81.06ZM10.41,35.42h7.8V6.92h10.1V0H.31v6.9h10.1Zm28.1,0h7.8V8.25h.1l9,27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2,23.1h-.1L50.31,0H38.51ZM152.43,55.67a15.07,15.07,0,0,0-4.52-5.52,18.57,18.57,0,0,0-6.68-3.08,33.54,33.54,0,0,0-8.07-1h-11.7v35.4h12.75a24.58,24.58,0,0,0,7.55-1.15A19.34,19.34,0,0,0,148.11,77a16.27,16.27,0,0,0,4.37-5.5,16.91,16.91,0,0,0,1.63-7.58A18.5,18.5,0,0,0,152.43,55.67ZM145,68.6A8.8,8.8,0,0,1,142.36,72a10.7,10.7,0,0,1-4,1.82,21.57,21.57,0,0,1-5,.55h-4.05v-21h4.6a17,17,0,0,1,4.67.63,11.66,11.66,0,0,1,3.88,1.87A9.14,9.14,0,0,1,145,59a9.87,9.87,0,0,1,1,4.52A11.89,11.89,0,0,1,145,68.6Zm44.63-.13a8,8,0,0,0-1.58-2.62A8.38,8.38,0,0,0,185.63,64a10.31,10.31,0,0,0-3.17-1v-.1a9.22,9.22,0,0,0,4.42-2.82,7.43,7.43,0,0,0,1.68-5,8.42,8.42,0,0,0-1.15-4.65,8.09,8.09,0,0,0-3-2.72,12.56,12.56,0,0,0-4.18-1.3,32.84,32.84,0,0,0-4.62-.33h-13.2v35.4h14.5a22.41,22.41,0,0,0,4.72-.5,13.53,13.53,0,0,0,4.28-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68A9.39,9.39,0,0,0,189.66,68.47ZM170.21,52.72h5.3a10,10,0,0,1,1.85.18,6.18,6.18,0,0,1,1.7.57,3.39,3.39,0,0,1,1.22,1.13,3.22,3.22,0,0,1,.48,1.82,3.63,3.63,0,0,1-.43,1.8,3.4,3.4,0,0,1-1.12,1.2,4.92,4.92,0,0,1-1.58.65,7.51,7.51,0,0,1-1.77.2h-5.65Zm11.72,20a3.9,3.9,0,0,1-1.22,1.3,4.64,4.64,0,0,1-1.68.7,8.18,8.18,0,0,1-1.82.2h-7v-8h5.9a15.35,15.35,0,0,1,2,.15,8.47,8.47,0,0,1,2.05.55,4,4,0,0,1,1.57,1.18,3.11,3.11,0,0,1,.63,2A3.71,3.71,0,0,1,181.93,72.72Z"/></g></g></svg> \ No newline at end of file diff --git a/movie-group-8/src/assets/default_avatar.png b/movie-group-8/src/assets/default_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..778e051529c32dc9e845d961a9229d0addaf09ea Binary files /dev/null and b/movie-group-8/src/assets/default_avatar.png differ diff --git a/movie-group-8/src/assets/home_poster.png b/movie-group-8/src/assets/home_poster.png new file mode 100644 index 0000000000000000000000000000000000000000..f5eb71833364c1f645d9d9a373d302325f6ab944 Binary files /dev/null and b/movie-group-8/src/assets/home_poster.png differ diff --git a/movie-group-8/src/assets/review_poster.png b/movie-group-8/src/assets/review_poster.png new file mode 100644 index 0000000000000000000000000000000000000000..64d338c03a1f9c348fbebc5fd6b36b13eb37bd29 Binary files /dev/null and b/movie-group-8/src/assets/review_poster.png differ diff --git a/movie-group-8/src/assets/tmdb_poster.png b/movie-group-8/src/assets/tmdb_poster.png new file mode 100644 index 0000000000000000000000000000000000000000..b3caa85d45a1b549aba2128421978d23fd5f0325 Binary files /dev/null and b/movie-group-8/src/assets/tmdb_poster.png differ diff --git a/movie-group-8/src/components/MovieCard.vue b/movie-group-8/src/components/MovieCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..15e6322b2e79283be5c16313a8484ba56dd24151 --- /dev/null +++ b/movie-group-8/src/components/MovieCard.vue @@ -0,0 +1,57 @@ +<template> + <div class="bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg p-4 flex gap-4 items-start"> + <!-- Clickable poster --> + <router-link + :to="`/films/${movie.id}`" + class="shrink-0 hover:opacity-90 transition" + > + <img + v-if="movie.poster_path" + :src="'https://image.tmdb.org/t/p/w154' + movie.poster_path" + alt="Poster" + class="w-24 h-36 object-cover rounded" + /> + </router-link> + + <div class="flex-1"> + <!-- Clickable title --> + <router-link + :to="`/films/${movie.id}`" + class="text-lg font-semibold text-gray-800 dark:text-white hover:underline block" + > + {{ movie.title }} + </router-link> + + <p class="text-sm text-gray-600 dark:text-neutral-400">â {{ movie.vote_average }}</p> + + <!-- Interactive status dropdown --> + <label class="text-xs text-gray-500 dark:text-gray-400 block mt-2"> + Status: + <select + v-model="selectedStatus" + @change="emitChange" + class="ml-2 bg-white dark:bg-neutral-700 border border-gray-300 dark:border-neutral-600 rounded px-2 py-1 text-sm" + > + <option value="planned">Planned</option> + <option value="watched">Watched</option> + </select> + </label> + </div> + </div> +</template> + +<script setup> +import { ref, watch } from 'vue' + +const props = defineProps({ + movie: Object, +}) + +const emit = defineEmits(['status-changed']) + +const selectedStatus = ref(props.movie.status) + +const emitChange = () => { + emit('status-changed', props.movie.id, selectedStatus.value) +} +</script> \ No newline at end of file diff --git a/movie-group-8/src/components/MovieList.vue b/movie-group-8/src/components/MovieList.vue new file mode 100644 index 0000000000000000000000000000000000000000..78b0a9e2d4e619722113da6978d27ccaae015ff9 --- /dev/null +++ b/movie-group-8/src/components/MovieList.vue @@ -0,0 +1,119 @@ +<template> + <div class="movie-list"> + <div v-if="!movies.length" class="no-results"> + <slot name="empty">No movies found.</slot> + </div> + <ul v-else class="movies"> + <li v-for="movie in movies" :key="movie.id" class="movie-item"> + <router-link + :to="{ name: 'FilmDetails', params: { id: movie.id } }" + class="movie-link" + > + <img + v-if="movie.poster_path" + :src="getPosterUrl(movie.poster_path)" + :alt="`${movie.title} poster`" + class="poster" + /> + <div class="details"> + <h2 class="title">{{ movie.title }}</h2> + <p class="release-date">{{ formatDate(movie.release_date) }}</p> + <p class="overview">{{ movie.overview || 'No overview available.' }}</p> + </div> + </router-link> + + </li> + </ul> + </div> +</template> + +<script setup> + +const props = defineProps({ + movies: { + type: Array, + required: true + } +}) + +// TMDB image base URL from Vite env or fallback +const IMAGE_BASE = import.meta.env.VITE_TMDB_IMAGE_BASE || 'https://image.tmdb.org/t/p/w200' + +/** + * Build full poster URL + */ +function getPosterUrl(path) { + return `${IMAGE_BASE}${path}` +} + +/** + * Format a date string to a more readable form + */ +function formatDate(dateString) { + if (!dateString) return 'Unknown' + const options = { year: 'numeric', month: 'long', day: 'numeric' } + return new Date(dateString).toLocaleDateString(undefined, options) +} +</script> + +<style scoped> +.movie-list { + display: flex; + flex-direction: column; +} + +.no-results { + text-align: center; + color: #666; + font-style: italic; +} + +.movies { + list-style: none; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; +} + +.movie-item { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.poster { + width: 100%; + object-fit: cover; + aspect-ratio: 2 / 3; +} + +.details { + padding: 0.75rem; + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.title { + font-size: 1.1rem; + margin: 0 0 0.5rem; +} + +.release-date { + font-size: 0.875rem; + color: #888; + margin: 0 0 0.5rem; +} + +.overview { + font-size: 0.9rem; + color: #444; + flex-grow: 1; +} + + +</style> diff --git a/movie-group-8/src/components/Navbar.vue b/movie-group-8/src/components/Navbar.vue new file mode 100644 index 0000000000000000000000000000000000000000..64d7e5700bc6baec7c66c76e9357bbc787de6f9e --- /dev/null +++ b/movie-group-8/src/components/Navbar.vue @@ -0,0 +1,130 @@ +<template> + <nav class="fixed top-0 left-0 right-0 min-w-11/12 mx-6 bg-neutral-800 text-white shadow-md px-4 flex justify-between items-center z-50 rounded-2xl mt-3"> + <!-- Logo --> + <div class="flex items-center"> + <RouterLink to="/"> + <img class="h-14 w-auto" src="../assets/Dark_Mode.png" alt="Logo" /> + </RouterLink> + </div> + + <!-- Search Bar (Perfectly Centered) --> + <div class="absolute left-1/2 transform -translate-x-1/2 w-full max-w-md"> + <input + v-model="searchQuery" + type="text" + placeholder="Search..." + class="w-full px-4 py-2 pr-10 rounded-full bg-neutral-700 text-white placeholder-gray-400 focus:ring-2 focus:ring-white focus:outline-none" + /> + <!-- Search Icon inside input --> + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" + class="size-6 absolute right-3 top-1/2 -translate-y-1/2 text-gray-400"> + <path stroke-linecap="round" stroke-linejoin="round" + d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /> + </svg> + </div> + + <!-- Navigation & Profile Section --> + <div class="flex items-center space-x-4 ml-auto"> + <!-- Navigation Links --> + <div class="flex space-x-3"> + <RouterLink + v-for="item in filteredNavigation" + :key="item.name" + :to="item.href" + class="px-3 py-2 rounded-md text-sm font-medium transition hover:bg-neutral-700" + :class="{ 'bg-neutral-900': item.href === currentRoute }"> + {{ item.name }} + </RouterLink> + + <RouterLink + v-if="!isLoggedIn" + to="/login" + class="px-4 py-2 rounded-xl bg-blue-600 text-white font-semibold text-sm transition hover:bg-blue-700 shadow-md"> + Login + </RouterLink> + </div> + + <!-- Profile Dropdown (Only show if logged in) --> + <Menu as="div" class="relative" v-if="isLoggedIn"> + <div> + <MenuButton class="flex rounded-full bg-gray-800 text-sm focus:ring-1 focus:ring-white focus:ring-offset-2"> + <img class="h-8 w-8 rounded-full" :src="userPhotoURL" alt="User Avatar" /> + </MenuButton> + </div> + <transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95"> + <MenuItems class="absolute right-0 mt-2 w-48 bg-neutral-800 rounded-md shadow-lg ring-1 ring-black/5 py-1"> + <MenuItem v-slot="{ active }"> + <RouterLink to="/profile" :class="[active ? 'bg-neutral-700' : '', 'block px-4 py-2 text-sm text-white']">Your Profile</RouterLink> + </MenuItem> + <MenuItem v-slot="{ active }"> + <RouterLink to="/settings" :class="[active ? 'bg-neutral-700' : '', 'block px-4 py-2 text-sm text-white']">Settings</RouterLink> + </MenuItem> + <MenuItem @click="handleSignOut" v-slot="{ active }"> + <a href="#" @click.prevent="handleSignOut" :class="[active ? 'bg-neutral-700' : '', 'block px-4 py-2 text-sm text-white']">Sign out</a> + </MenuItem> + </MenuItems> + </transition> + </Menu> + </div> + </nav> +</template> + +<script setup> +import { ref, computed, onMounted } from 'vue'; +import { useRoute, useRouter } from 'vue-router'; +import { getAuth, onAuthStateChanged, signOut } from 'firebase/auth'; +import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'; +import defaultAvatar from '@/assets/default_avatar.png' + +// Firebase Authentication instance +const auth = getAuth(); + +const route = useRoute(); +const router = useRouter(); +const isLoggedIn = ref(false); +const userPhotoURL = ref(''); + +const navigation = ref([ + { name: 'Films', href: '/films', authRequired: true }, + { name: 'Watchlist', href: '/watchlist', authRequired: true }, + { name: 'Top Rated', href: '/top-rated', authRequired: true }, + { name: 'Social', href: '/social', authRequired: true }, + +]); + +// Compute the current route to match against the navigation links +const currentRoute = computed(() => route.path); + +// Filter navigation based on auth state +const filteredNavigation = computed(() => { + return navigation.value.filter(item => { + if (item.authRequired && !isLoggedIn.value) return false; // Hide if user is not logged in + if (item.guestOnly && isLoggedIn.value) return false; // Hide if user is logged in + return true; + }); +}); + +// Check authentication state +onMounted(() => { + onAuthStateChanged(auth, (user) => { + isLoggedIn.value = !!user; // Sets to true if user exists, false otherwise + if(user && user.photoURL) { + userPhotoURL.value = user.photoURL; + } else { + userPhotoURL.value = defaultAvatar; + } + }); +}); + +// Sign-out function +const handleSignOut = () => { + signOut(auth) + .then(() => { + isLoggedIn.value = false; // Ensure UI updates correctly + router.push('/'); // Redirect to home + }) + .catch((error) => { + console.error("Error signing out:", error); + }); +}; +</script> diff --git a/movie-group-8/src/components/TopRatedMovies.vue b/movie-group-8/src/components/TopRatedMovies.vue new file mode 100644 index 0000000000000000000000000000000000000000..090e315b6344a73ba91f116b5f0b5aad23b124a1 --- /dev/null +++ b/movie-group-8/src/components/TopRatedMovies.vue @@ -0,0 +1,145 @@ +<script setup> +import { ref, onMounted, watch } from 'vue' + +const year = ref(2000) +const genre = ref('') +const genres = ref([]) +const movies = ref([]) + +const TMDB_API_KEY = import.meta.env.VITE_TMDB_API_KEY + +// Fetch genres on component mount +onMounted(async () => { + const genreRes = await fetch(`https://api.themoviedb.org/3/genre/movie/list?api_key=${TMDB_API_KEY}`) + const genreData = await genreRes.json() + genres.value = genreData.genres +}) + +// Watch for changes in year or genre and fetch movies +watch([year, genre], async () => { + const url = `https://api.themoviedb.org/3/discover/movie?api_key=${TMDB_API_KEY}&sort_by=vote_average.desc&vote_count.gte=50&primary_release_year=${year.value}` + + (genre.value ? `&with_genres=${genre.value}` : '') + + const res = await fetch(url) + const data = await res.json() + movies.value = data.results +}, { immediate: true }) +</script> + +<template> + <div class="nyt-header"> + <h1>The Movies We've Loved Since 2000</h1> + <p> + Explore top-rated films using the filters below — by genre and year — and rediscover hidden gems. + </p> + </div> + + <div class="filters"> + <select v-model="genre"> + <option value="">Pick a genre ...</option> + <option v-for="g in genres" :key="g.id" :value="g.id">{{ g.name }}</option> + </select> + + <select v-model="year"> + <option v-for="y in Array.from({length: 25}, (_, i) => 2000 + i)" :key="y" :value="y"> + {{ y }} + </option> + </select> + </div> + + <h2 class="section-title">Our favorite movies from {{ year }}</h2> + + <div class="movie-grid"> + <router-link + v-for="movie in movies" + :key="movie.id" + :to="`/films/${movie.id}`" + class="movie-card block hover:opacity-90 transition" + > + <img + v-if="movie.poster_path" + :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path" + alt="poster" + /> + <p class="title">{{ movie.title }}</p> + <p class="rating">â {{ movie.vote_average }}</p> + </router-link> + </div> +</template> + +<style scoped> +.nyt-header { + text-align: center; + max-width: 700px; + margin: 3rem auto 2rem auto; + padding: 0 1rem; +} +.nyt-header h1 { + font-size: 2.4rem; + font-weight: 700; + line-height: 1.2; +} +.nyt-header p { + font-size: 1.1rem; + margin-top: 0.75rem; + color: #555; +} + +.filters { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 1rem; + margin-bottom: 2rem; +} + +.filters select { + padding: 0.5rem 1.2rem; + background-color: #cce8ff; + border: none; + border-radius: 8px; + font-weight: bold; + font-size: 1rem; + color: #003366; + cursor: pointer; + transition: background-color 0.2s ease; +} +.filters select:hover { + background-color: #b2dcff; +} + +.section-title { + text-align: center; + font-size: 1.6rem; + font-weight: bold; + margin-bottom: 1.5rem; +} + +.movie-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 1.2rem; + padding: 0 1rem; +} + +.movie-card { + text-align: center; +} + +.movie-card img { + width: 100%; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); +} + +.title { + font-weight: 600; + margin-top: 0.5rem; +} + +.rating { + color: #ffa500; + font-size: 0.9rem; +} + +</style> diff --git a/movie-group-8/src/components/Watchlist.vue b/movie-group-8/src/components/Watchlist.vue new file mode 100644 index 0000000000000000000000000000000000000000..724082258be8667655500d595f6361458acab0c9 --- /dev/null +++ b/movie-group-8/src/components/Watchlist.vue @@ -0,0 +1,89 @@ +<template> + <div class="watchlist"> + <h1>Your Watchlist</h1> + + <div class="filters"> + <select v-model="filter"> + <option value="">All</option> + <option value="planned">Plan To Watch</option> + <option value="watched">Watched</option> + </select> + </div> + + <div class="movie-grid"> + <div + v-for="movie in filteredWatchlist" + :key="movie.id" + class="movie-card" + > + <img :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path" alt="poster" /> + <p class="title">{{ movie.title }}</p> + <p class="rating">â {{ movie.vote_average }}</p> + <p class="status">📌 {{ movie.status }}</p> + <button + @click="removeFromWatchlist(movie)" + class="mt-2 px-3 py-1 bg-red-600 text-white rounded hover:bg-red-700 transition" + > + Remove + </button> + </div> + </div> + </div> +</template> + +<script setup> + import { ref, computed, onMounted } from 'vue' + import { getFirestore, collection, getDocs, doc, deleteDoc } from 'firebase/firestore' + import { getAuth } from 'firebase/auth' + + const db = getFirestore() + const auth = getAuth() + + const watchlist = ref([]) + const filter = ref('') + + const fetchWatchlist = async () => { + const user = auth.currentUser + if (!user) return + + const snapshot = await getDocs(collection(db, 'users', user.uid, 'watchlist')) + watchlist.value = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) + } + + const removeFromWatchlist = async (movie) => { + const user = auth.currentUser + const movieRef = doc(db, 'users', user.uid, 'watchlist', String(movie.id)) + await deleteDoc(movieRef) + + // remove locally + watchlist.value = watchlist.value.filter(m => m.id !== movie.id) + alert(`Removed "${movie.title}" from your watchlist.`) + } + + const filteredWatchlist = computed(() => { + if (!filter.value) return watchlist.value + return watchlist.value.filter(movie => movie.status === filter.value) + }) + + onMounted(() => { + fetchWatchlist() + }) +</script> + +<style scoped> + .watchlist { + padding: 2rem; + text-align: center; + } + + .filters { + margin-bottom: 1rem; + } + + .movie-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 1.2rem; + } +</style> + \ No newline at end of file diff --git a/movie-group-8/src/components/WatchlistButton.vue b/movie-group-8/src/components/WatchlistButton.vue new file mode 100644 index 0000000000000000000000000000000000000000..91458232887237079c738f99224113fc42242682 --- /dev/null +++ b/movie-group-8/src/components/WatchlistButton.vue @@ -0,0 +1,37 @@ +<template> + <button + type="button" + @click="toggleWatchlist" + class="mt-2 px-3 py-1 rounded text-white text-sm transition" + :class="isInWatchlist + ? 'bg-red-600 hover:bg-red-700' + : 'bg-green-600 hover:bg-green-700'" + > + {{ isInWatchlist ? 'Remove from Watchlist' : 'Add to Watchlist' }} + </button> +</template> + +<script setup> +import { ref, watch } from 'vue' +import { useWatchlist } from '@/composables/useWatchlist.js' + +const props = defineProps({ + movie: { + type: Object, + required: true + } +}) + +// We need a ref for the ID and a ref for the data +const movieId = ref(props.movie.id) +const movieData = ref(props.movie) + +// Pull in the composable +const { isInWatchlist, toggleWatchlist } = useWatchlist(movieId, movieData) + +// If parent updates the prop, keep our ref in sync +watch(() => props.movie, m => { + movieId.value = m.id + movieData.value = m +}) +</script> \ No newline at end of file diff --git a/movie-group-8/src/composables/useMovies,js b/movie-group-8/src/composables/useMovies,js new file mode 100644 index 0000000000000000000000000000000000000000..673d1e96b8d09008d2879a81c74758a1d19ac4a8 --- /dev/null +++ b/movie-group-8/src/composables/useMovies,js @@ -0,0 +1,29 @@ +import axios from 'axios' +import { ref } from 'vue' + +const API_KEY = process.env.VITE_TMDB_API_KEY +const BASE = process.env.VUE_APP_TMDB_BASE + +export function useMovies() { + const results = ref([]) + const loading = ref(false) + const error = ref(null) + + async function search(query) { + loading.value = true + error.value = null + try { + const url = `${BASE}/search/movie` + const { data } = await axios.get(url, { + params: { api_key: API_KEY, query } + }) + results.value = data.results + } catch (e) { + error.value = e + } finally { + loading.value = false + } + } + + return { results, loading, error, search } +} \ No newline at end of file diff --git a/movie-group-8/src/composables/useReviews.js b/movie-group-8/src/composables/useReviews.js new file mode 100644 index 0000000000000000000000000000000000000000..04f72c7ee314c80235aaeccadff5d23ae4bb073d --- /dev/null +++ b/movie-group-8/src/composables/useReviews.js @@ -0,0 +1,29 @@ +// src/composables/useReviews.js +import { doc, getDoc, setDoc, collectionGroup, query, where, getDocs } from 'firebase/firestore' +import { auth, db } from '@/firebase.js' + +export async function getUserReview(movieId) { + const user = auth.currentUser + if (!user) throw new Error('User not authenticated') + + const ref = doc(db, 'users', user.uid, 'reviews', movieId) + const snap = await getDoc(ref) + return snap.exists() ? snap.data() : null +} + +export async function submitReview({ movieId, rating, text }) { + const user = auth.currentUser + if (!user) throw new Error('User not authenticated') + + const ref = doc(db, 'users', user.uid, 'reviews', movieId) + await setDoc(ref, { movieId, rating, text, authorName: user.displayName, authorPFP: user.photoURL }, { merge: true }) +} + +export async function getAllReviews(movieId) { + const q = query( + collectionGroup(db, 'reviews'), + where('movieId', '==', movieId) + ) + const snaps = await getDocs(q) + return snaps.docs.map(d => ({ id: d.id, ...d.data() })) +} diff --git a/movie-group-8/src/composables/useWatchlist.js b/movie-group-8/src/composables/useWatchlist.js new file mode 100644 index 0000000000000000000000000000000000000000..7dc09f0315ba6d966362d22a40f381bbf7b30da6 --- /dev/null +++ b/movie-group-8/src/composables/useWatchlist.js @@ -0,0 +1,47 @@ +import { ref, watchEffect } from 'vue' +import { getAuth } from 'firebase/auth' +import { getFirestore, doc, getDoc, setDoc, deleteDoc } from 'firebase/firestore' + +export function useWatchlist(movieId, movieData) { + const auth = getAuth() + const db = getFirestore() + const isInWatchlist = ref(false) + + // Check on load & whenever movieId or user changes + watchEffect(async (onInvalidate) => { + const user = auth.currentUser + if (!user.value ?? user) { + isInWatchlist.value = false + return + } + const ref = doc(db, 'users', user.uid, 'watchlist', String(movieId.value ?? movieId)) + const snap = await getDoc(ref) + isInWatchlist.value = snap.exists() + }) + + // Toggle function + const toggleWatchlist = async () => { + const user = auth.currentUser + if (!user) { + return alert('You need to log in.') + } + const ref = doc(db, 'users', user.uid, 'watchlist', String(movieId.value ?? movieId)) + + if (isInWatchlist.value) { + await deleteDoc(ref) + isInWatchlist.value = false + alert('Removed from watchlist') + } else { + await setDoc(ref, { + title: movieData.value.title, + poster_path: movieData.value.poster_path, + vote_average: movieData.value.vote_average, + status: 'planned' + }) + isInWatchlist.value = true + alert('Added to watchlist') + } + } + + return { isInWatchlist, toggleWatchlist } +} \ No newline at end of file diff --git a/movie-group-8/src/firebase.js b/movie-group-8/src/firebase.js new file mode 100644 index 0000000000000000000000000000000000000000..46b814efeb1edf7ccb8b1467ae6ea10211ee0d36 --- /dev/null +++ b/movie-group-8/src/firebase.js @@ -0,0 +1,20 @@ +import { initializeApp } from 'firebase/app' +import { getAuth } from 'firebase/auth' +import { getFirestore } from 'firebase/firestore' + +// Replace with your actual config (you can move the object here or import it) +const firebaseConfig = { + apiKey: "AIzaSyDiAEv_XsHelEiuvJsPDOEBZRcsr4Mg4R4", + authDomain: "movie-group-8.firebaseapp.com", + projectId: "movie-group-8", + storageBucket: "movie-group-8.firebasestorage.app", + messagingSenderId: "767837078763", + appId: "1:767837078763:web:e64ea7235d421e46852aae" + }; + +// Initialize (idempotent if called twice) +const app = initializeApp(firebaseConfig) + +// Export the singleton instances +export const auth = getAuth(app) +export const db = getFirestore(app) \ No newline at end of file diff --git a/movie-group-8/src/main.js b/movie-group-8/src/main.js new file mode 100644 index 0000000000000000000000000000000000000000..30fe276854aea0d9445bf901ad30693350a32187 --- /dev/null +++ b/movie-group-8/src/main.js @@ -0,0 +1,13 @@ +// main.js +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' + +// Ensure the Firebase singleton is initialized +import '@/firebase.js' + +import './style.css' + +createApp(App) + .use(router) + .mount('#app') \ No newline at end of file diff --git a/movie-group-8/src/router/index.js b/movie-group-8/src/router/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fcbec63700fcb695109d7a419820f479345ec6d5 --- /dev/null +++ b/movie-group-8/src/router/index.js @@ -0,0 +1,90 @@ +import { getAuth, onAuthStateChanged } from 'firebase/auth'; +import { createRouter, createWebHistory } from 'vue-router'; +import Films from '@/views/Films.vue' +import FilmDetails from '@/views/FilmDetails.vue' +import ReviewPage from '@/views/ReviewPage.vue' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + + + { path: '/', component: () => import('../views/Home.vue') }, + { path: '/top-rated', component: () => import('../views/TopRated.vue'), meta: { requiresAuth: true }}, + { path: '/register', component: () => import('../views/Register.vue') }, + { path: '/login', component: () => import('../views/Login.vue') }, + { path: '/recover-account', component: () => import('../views/RecoverAccount.vue') }, + { + path: '/films', + name: 'Films', + component: Films, + meta: { requiresAuth: true } + }, + { + path: '/films/:id', + name: 'FilmDetails', + component: FilmDetails, + }, + + { + path: '/films/:id/review', + name: 'ReviewFilm', + component: ReviewPage, + props: true, + }, + + { + path: '/watchlist', + component: () => import('../views/WatchlistView.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/profile', + component: () => import('../views/Profile.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/settings', + component: () => import('../views/Settings.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/social', + name: 'Social', + component: () => import('../views/Social.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/user/:id', + name: 'UserProfile', + component: () => import('../views/UserProfile.vue') + }, + ], +}); + +const getCurrentUser = () => { + return new Promise((resolve, reject) => { + const removeListener = onAuthStateChanged( + getAuth(), + user => { + removeListener(); + resolve(user); + }, + reject); + }); +}; + +router.beforeEach(async (to, from, next) => { + if (to.matched.some(record => record.meta.requiresAuth)) { + if (await getCurrentUser()) { + next(); + } else { + alert('You must be logged in to see this page'); + next("/"); + } + } else { + next(); + } +}); + +export default router; \ No newline at end of file diff --git a/movie-group-8/src/style.css b/movie-group-8/src/style.css new file mode 100644 index 0000000000000000000000000000000000000000..a461c505f1f0c24ab12240ac3f7fa374dfa237fb --- /dev/null +++ b/movie-group-8/src/style.css @@ -0,0 +1 @@ +@import "tailwindcss"; \ No newline at end of file diff --git a/movie-group-8/src/views/FilmDetails.vue b/movie-group-8/src/views/FilmDetails.vue new file mode 100644 index 0000000000000000000000000000000000000000..ebc1be66bed09a4459ba2f28e3a0edf377d69d5b --- /dev/null +++ b/movie-group-8/src/views/FilmDetails.vue @@ -0,0 +1,226 @@ +<template> + <div class="film-details"> + <button class="back-button" @click="$router.back()">↠Back</button> + + <div v-if="loading" class="loading">Loading movie...</div> + <div v-else-if="error" class="error">Error: {{ error }}</div> + + <div v-else class="details-container"> + <div class="poster-container"> + <img + v-if="movie.poster_path" + :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`" + :alt="`${movie.title} poster`" + class="poster" + /> + </div> + + <div class="info-container"> + <h1 class="title">{{ movie.title }}</h1> + <p class="release-date">{{ formatDate(movie.release_date) }}</p> + <p class="overview">{{ movie.overview }}</p> + + <div v-if="trailerUrl" class="trailer"> + <h2>Trailer</h2> + <iframe + :src="trailerUrl" + frameborder="0" + allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" + allowfullscreen + ></iframe> + </div> + + <button + type="button" + @click="toggleWatchlist" + class="mt-4 px-4 py-2 rounded text-white transition" + :class="isInWatchlist + ? 'bg-red-600 hover:bg-red-700' + : 'bg-green-600 hover:bg-green-700'" + > + {{ isInWatchlist ? 'Remove from Watchlist' : 'Add to Watchlist' }} + </button> + + <RouterLink + :to="{ name: 'ReviewFilm', params: { id: movie.id } }" + class="mt-2 inline-block px-4 py-2 bg-blue-600 rounded text-white hover:bg-blue-700 text-center" + > + Review this Film + </RouterLink> + <div class="mt-6 reviews-section"> + <h2 class="text-xl mb-2 text-white">User Reviews</h2> + <div v-if="reviewsLoading" class="text-gray-400">Loading reviews…</div> + <div v-else-if="!reviews.length" class="text-gray-400">No reviews yet.</div> + <ul v-else class="space-y-4"> + <li v-for="(r, i) in reviews" :key="i" class="bg-neutral-800 p-4 rounded"> + <div class="flex items-center mb-2"> + <img + v-if="r.authorPFP" + :src="r.authorPFP" + alt="" + class="h-8 w-8 rounded-full mr-2"/> + <span class="font-semibold text-white">{{ r.authorName }}</span> + <span class="text-gray-400 ml-2">{{ r.rating }}★</span> + </div> + <p class="text-gray-200">{{ r.text }}</p> + </li> + </ul> + </div> + </div> + </div> + </div> +</template> + +<script setup> +import { ref, onMounted } from 'vue' +import { useRoute } from 'vue-router' +import { useWatchlist } from '@/composables/useWatchlist.js' +import { getAuth } from 'firebase/auth' +import { getFirestore, doc, getDoc } from 'firebase/firestore' +import { getAllReviews } from '@/composables/useReviews.js' + +const route = useRoute() +const movie = ref(null) +const trailerUrl = ref('') +const loading = ref(true) +const error = ref('') +const movieId = ref(route.params.id) +const { isInWatchlist, toggleWatchlist } = useWatchlist(movieId, movie) +const reviews = ref([]) +const reviewsLoading = ref(true) + +function formatDate(dateString) { + if (!dateString) return 'Unknown' + return new Date(dateString).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric' + }) +} + +async function fetchMovieDetails(id) { + try { + const res = await fetch( + `${import.meta.env.VITE_TMDB_BASE}/movie/${id}?api_key=${import.meta.env.VITE_TMDB_API_KEY}` + ) + if (!res.ok) throw new Error('Failed to fetch movie details') + movie.value = await res.json() + } catch (err) { + error.value = err.message + } +} + +async function fetchMovieVideos(id) { + try { + const res = await fetch( + `${import.meta.env.VITE_TMDB_BASE}/movie/${id}/videos?api_key=${import.meta.env.VITE_TMDB_API_KEY}` + ) + if (!res.ok) throw new Error('Failed to fetch movie videos') + const data = await res.json() + const trailer = data.results.find(v => v.type === 'Trailer' && v.site === 'YouTube') + if (trailer) { + trailerUrl.value = `https://www.youtube.com/embed/${trailer.key}` + } + } catch { + // ignore trailer errors + } +} + +onMounted(async () => { + const id = route.params.id + await fetchMovieDetails(id) + await fetchMovieVideos(id) + + const res = await getAllReviews(id) + reviews.value = res + + const user = getAuth().currentUser + if (user) { + const db = getFirestore() + const watchRef = doc(db, 'users', user.uid, 'watchlist', String(id)) + const snap = await getDoc(watchRef) + isInWatchlist.value = snap.exists() + } + + reviewsLoading.value = false + loading.value = false +}) +</script> + +<style scoped> +.film-details { + background-color: #121212; + color: #ffffff; + min-height: 100vh; + padding: 2rem; + box-sizing: border-box; +} + +.back-button { + color: #ffffff; + background: transparent; + margin-bottom: 1rem; + font-size: 1rem; +} + +.loading, +.error { + color: #ffffff; + text-align: center; + margin: 2rem 0; +} + +.details-container { + display: flex; + flex-wrap: wrap; + gap: 2rem; + background-color: #1e1e1e; + padding: 2rem; + border-radius: 8px; + max-width: 900px; + margin: 2rem auto 0; +} + +.poster-container { + flex: 0 0 300px; +} + +.poster { + width: 100%; + border-radius: 4px; +} + +.info-container { + flex: 1; + display: flex; + flex-direction: column; +} + +.title { + font-size: 2rem; + margin: 0 0 0.5rem; +} + +.release-date { + color: #bbbbbb; + margin-bottom: 1rem; +} + +.overview { + color: #dddddd; + line-height: 1.6; + margin-bottom: 1.5rem; +} + +.trailer h2 { + margin-bottom: 0.5rem; + color: #ffffff; +} + +.trailer iframe { + width: 100%; + height: 300px; + border-radius: 8px; + border: none; +} +</style> diff --git a/movie-group-8/src/views/Films.vue b/movie-group-8/src/views/Films.vue new file mode 100644 index 0000000000000000000000000000000000000000..31474cbb988dd844fe1c2a8548fef94c70db2bfa --- /dev/null +++ b/movie-group-8/src/views/Films.vue @@ -0,0 +1,106 @@ +<template> + <div class="films-container"> + <!-- Centered search bar pushed down slightly --> + <div class="search-wrapper"> + <input + v-model="term" + @keyup.enter="onSearch" + placeholder="Search movies…" + class="search-input" + /> + </div> + + <div v-if="loading" class="loading">Loading…</div> + <div v-else-if="error" class="error">Error: {{ error.message }}</div> + + <!-- Movie grid --> + <MovieList :movies="results" /> + </div> +</template> + +<script setup> +import { ref, onMounted } from 'vue' +import axios from 'axios' +import MovieList from '../components/MovieList.vue' + +const API_KEY = import.meta.env.VITE_TMDB_API_KEY +const BASE = import.meta.env.VITE_TMDB_BASE + +const term = ref('') +const results = ref([]) +const loading = ref(false) +const error = ref(null) + +async function search(query) { + loading.value = true + error.value = null + try { + const { data } = await axios.get(`${BASE}/search/movie`, { + params: { api_key: API_KEY, query } + }) + results.value = data.results + } catch (e) { + error.value = e + } finally { + loading.value = false + } +} + +async function fetchPopular() { + loading.value = true + error.value = null + try { + const { data } = await axios.get(`${BASE}/movie/popular`, { + params: { api_key: API_KEY } + }) + results.value = data.results + } catch (e) { + error.value = e + } finally { + loading.value = false + } +} + +function onSearch() { + if (term.value.trim()) { + search(term.value) + } else { + fetchPopular() + } +} + +onMounted(fetchPopular) +</script> + +<style scoped> +.films-container { + padding-top: 6rem; /* adjust to nav height + spacing */ + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.search-wrapper { + width: 100%; + max-width: 500px; + margin-bottom: 1.5rem; + display: flex; + justify-content: center; +} + +.search-input { + width: 100%; + padding: 0.75rem 1rem; + font-size: 1.1rem; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.loading, +.error { + margin: 1rem 0; + font-weight: bold; +} +</style> diff --git a/movie-group-8/src/views/Home.vue b/movie-group-8/src/views/Home.vue new file mode 100644 index 0000000000000000000000000000000000000000..90271525376142187d497587ad17fb21e1fc930b --- /dev/null +++ b/movie-group-8/src/views/Home.vue @@ -0,0 +1,75 @@ +<template> + <div class="h-screen bg-neutral-900 p-6"> + <div class="max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto dark"> + <div class="relative p-6 md:p-16"> + <div class="relative z-10 lg:grid lg:grid-cols-12 lg:gap-16 lg:items-center"> + <div class="mb-10 lg:mb-0 lg:col-span-6 lg:col-start-8 lg:order-2"> + <h2 class="text-2xl text-neutral-200 font-bold sm:text-3xl"> + Welcome to SceneIt! Your new portal for all your movie needs. + </h2> + <nav class="grid gap-4 mt-5 md:mt-10" aria-label="Tabs" role="tablist" aria-orientation="vertical"> + <button @click="setTab('list')" :class="{ 'bg-neutral-700 shadow-md': activeTab === 'list' }" class="text-start hover:bg-neutral-700 focus:bg-neutral-700 p-4 md:p-5 rounded-xl"> + <span class="flex gap-x-6"> + <svg class="shrink-0 mt-2 size-6 md:size-7 text-blue-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"/><path d="M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"/><path d="M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"/><path d="M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"/><path d="M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"/></svg> + <span class="grow"> + <span class="block text-lg font-semibold text-blue-500">Movie Lists</span> + <span class="block mt-1 text-neutral-200">Organize your favorites, track what you've seen, and never forget a great film again!</span> + </span> + </span> + </button> + <button @click="setTab('reviews')" :class="{ 'bg-neutral-700 shadow-md': activeTab === 'reviews' }" class="text-start hover:bg-neutral-700 focus:bg-neutral-700 p-4 md:p-5 rounded-xl"> + <span class="flex gap-x-6"> + <svg class="shrink-0 mt-2 size-6 md:size-7 text-blue-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg> + <span class="grow"> + <span class="block text-lg font-semibold text-blue-500">Movie Reviews</span> + <span class="block mt-1 text-neutral-200">Read reviews from fellow movie lovers or share your own.</span> + </span> + </span> + </button> + <button @click="setTab('tmdb')" :class="{ 'bg-neutral-700 shadow-md': activeTab === 'tmdb' }" class="text-start hover:bg-neutral-700 focus:bg-neutral-700 p-4 md:p-5 rounded-xl"> + <span class="flex gap-x-6"> + <img class="h-10 w-auto" src="../assets/TMDB.svg" alt="TMDb Logo" /> + <span class="grow"> + <span class="block text-lg font-semibold text-blue-500">Powered by TMDB</span> + <span class="block mt-1 text-neutral-200">Data provided by The Movie Database.</span> + </span> + </span> + </button> + </nav> + </div> + <div class="lg:col-span-6"> + <div class="relative"> + <img v-if="activeTab === 'list'" class="shadow-xl shadow-gray-900/20 rounded-xl object-cover w-[560px] h-[720px]" src="../assets/home_poster.png" alt="Movie Lists"> + <img v-if="activeTab === 'reviews'" class="shadow-xl shadow-gray-900/20 rounded-xl object-cover w-[560px] h-[720px]" src="../assets/review_poster.png" alt="Movie Reviews"> + <img v-if="activeTab === 'tmdb'" class="shadow-xl shadow-gray-900/20 rounded-xl object-cover w-[560px] h-[720px]" src="../assets/tmdb_poster.png" alt="Powered by TMDB"> + </div> + </div> + </div> + <div class="absolute inset-0 bg-neutral-800 rounded-xl"></div> + </div> + </div> + </div> +</template> + +<script> +export default { +data() { + return { + activeTab: 'list', + tabs: ['list', 'reviews', 'tmdb'], + }; +}, +methods: { + setTab(tab) { + this.activeTab = tab; + } +}, +mounted() { + setInterval(() => { + const currentIndex = this.tabs.indexOf(this.activeTab); + const nextIndex = (currentIndex + 1) % this.tabs.length; + this.activeTab = this.tabs[nextIndex]; + }, 5000); +} +}; +</script> \ No newline at end of file diff --git a/movie-group-8/src/views/Login.vue b/movie-group-8/src/views/Login.vue new file mode 100644 index 0000000000000000000000000000000000000000..0b539b1f8a6880c589d20a59837fc7afc0c1f5fc --- /dev/null +++ b/movie-group-8/src/views/Login.vue @@ -0,0 +1,143 @@ +<template> + <div class="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-neutral-900"> + <div class="mt-7 bg-white border border-gray-200 rounded-xl shadow-2xs dark:bg-neutral-900 dark:border-neutral-700 min-w-96"> + <div class="p-4 sm:p-7"> + <div class="text-center"> + <h1 class="block text-2xl font-bold text-gray-800 dark:text-white">Login</h1> + <p class="mt-2 text-sm text-gray-600 dark:text-neutral-400"> + Don't have an account? + <a class="text-blue-600 decoration-2 hover:underline focus:outline-hidden focus:underline font-medium dark:text-blue-500" href="/register"> + Sign up here + </a> + </p> + </div> + + <div class="mt-5"> + <button @click="signInWithGoogle" type="button" class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800"> + <svg class="w-4 h-auto" width="46" height="47" viewBox="0 0 46 47" fill="none"> + <path d="M46 24.0287C46 22.09 45.8533 20.68 45.5013 19.2112H23.4694V27.9356H36.4069C36.1429 30.1094 34.7347 33.37 31.5957 35.5731L31.5663 35.8669L38.5191 41.2719L38.9885 41.3306C43.4477 37.2181 46 31.1669 46 24.0287Z" fill="#4285F4"/> + <path d="M23.4694 47C29.8061 47 35.1161 44.9144 39.0179 41.3012L31.625 35.5437C29.6301 36.9244 26.9898 37.8937 23.4987 37.8937C17.2793 37.8937 12.0281 33.7812 10.1505 28.1412L9.88649 28.1706L2.61097 33.7812L2.52296 34.0456C6.36608 41.7125 14.287 47 23.4694 47Z" fill="#34A853"/> + <path d="M10.1212 28.1413C9.62245 26.6725 9.32908 25.1156 9.32908 23.5C9.32908 21.8844 9.62245 20.3275 10.0918 18.8588V18.5356L2.75765 12.8369L2.52296 12.9544C0.909439 16.1269 0 19.7106 0 23.5C0 27.2894 0.909439 30.8731 2.49362 34.0456L10.1212 28.1413Z" fill="#FBBC05"/> + <path d="M23.4694 9.07688C27.8699 9.07688 30.8622 10.9863 32.5344 12.5725L39.1645 6.11C35.0867 2.32063 29.8061 0 23.4694 0C14.287 0 6.36607 5.2875 2.49362 12.9544L10.0918 18.8588C11.9987 13.1894 17.25 9.07688 23.4694 9.07688Z" fill="#EB4335"/> + </svg> + Sign in with Google + </button> + + <div class="py-3 flex items-center text-xs text-gray-400 uppercase before:flex-1 before:border-t before:border-gray-200 before:me-6 after:flex-1 after:border-t after:border-gray-200 after:ms-6 dark:text-neutral-500 dark:before:border-neutral-600 dark:after:border-neutral-600">Or</div> + + <form @submit.prevent="login"> + <div class="grid gap-y-4"> + <div> + <label class="block text-sm mb-2 dark:text-white">Email Address</label> + <input type="email" v-model="email" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required /> + </div> + + <div> + <div class="flex justify-between items-center mb-2"> + <label class="block text-sm dark:text-white">Password</label> + <a class="text-sm text-blue-600 decoration-2 hover:underline focus:outline-hidden focus:underline font-medium dark:text-blue-500" href="/recover-account"> + Forgot password? + </a> + </div> + <input type="password" v-model="password" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required /> + </div> + + <button + type="submit" + @click="login" + :disabled="loading" + class="w-full py-3 bg-blue-600 text-white rounded-lg flex justify-center items-center gap-2 hover:bg-blue-700 transition duration-300 disabled:opacity-50 disabled:pointer-events-none" + > + <svg v-if="loading" class="w-5 h-5 animate-spin text-white" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="opacity-25"/> + <path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="opacity-75"/> + </svg> + <span v-if="!loading">Sign in</span> + <span v-else>Loading...</span> + </button> + </div> + </form> + </div> + </div> + </div> + </div> +</template> + +<script setup> +import { ref } from 'vue'; +import { + getAuth, + signInWithEmailAndPassword, + signInWithPopup, + GoogleAuthProvider +} from 'firebase/auth'; +import { + getFirestore, + doc, + getDoc, + setDoc +} from 'firebase/firestore'; +import { useRouter } from 'vue-router'; +import defaultAvatar from '@/assets/default_avatar.png' + +const email = ref(''); +const password = ref(''); +const router = useRouter(); +const loading = ref(false); + +const db = getFirestore(); + +const createUserIfNotExists = async (user) => { + const userRef = doc(db, 'users', user.uid) + const userSnap = await getDoc(userRef) + + if (!userSnap.exists()) { + const photoURLToSave = user.photoURL || defaultAvatar + + await setDoc(userRef, { + uid: user.uid, + displayName: user.displayName || 'Anonymous', + photoURL: photoURLToSave + }) + } +} + +const login = async () => { + try { + loading.value = true; + const auth = getAuth(); + const userCredential = await signInWithEmailAndPassword(auth, email.value, password.value); + const user = userCredential.user; + + await createUserIfNotExists(user); + + console.log('Successfully logged in!'); + router.push('/films'); + } catch (error) { + console.error(error); + alert(error.message); + } finally { + loading.value = false; + } +}; + +const signInWithGoogle = async () => { + try { + loading.value = true; + const auth = getAuth(); + const provider = new GoogleAuthProvider(); + const result = await signInWithPopup(auth, provider); + const user = result.user; + + await createUserIfNotExists(user); + + console.log('Successfully signed in with Google!'); + router.push('/films'); + } catch (error) { + console.error(error); + alert(error.message); + } finally { + loading.value = false; + } +}; +</script> \ No newline at end of file diff --git a/movie-group-8/src/views/Profile.vue b/movie-group-8/src/views/Profile.vue new file mode 100644 index 0000000000000000000000000000000000000000..94358a61e2fbb94778cbbba0fb77cd7cdc36f920 --- /dev/null +++ b/movie-group-8/src/views/Profile.vue @@ -0,0 +1,321 @@ +<template> + <div class="min-h-screen bg-gray-100 dark:bg-neutral-900 text-center px-4 py-8"> + <div class="text-center mb-10"> + <img + v-if="userPhotoURL" + :src="userPhotoURL" + alt="Profile Picture" + class="w-28 h-28 mx-auto rounded-full object-cover mb-4" + /> + <h1 class="text-3xl font-bold text-gray-800 dark:text-white">{{ displayName || 'Anonymous' }}</h1> + <p class="text-sm text-gray-600 dark:text-neutral-400">{{ userEmail }}</p> + <p class="mt-2 text-base text-gray-600 dark:text-neutral-400">{{ description }}</p> + + <button + @click="showForm = !showForm" + class="mt-4 px-5 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition" + > + {{ showForm ? 'Close Update Form' : 'Update Profile' }} + </button> + </div> + + <!-- Update Form --> + <div v-if="showForm" class="max-w-xl mx-auto mb-12 bg-white dark:bg-neutral-800 p-6 rounded-xl border dark:border-neutral-700"> + <form @submit.prevent="handleUpdateProfile" class="grid gap-6"> + <div> + <label class="block text-sm mb-2 dark:text-white">Display Name</label> + <input type="text" v-model="displayName" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required /> + </div> + + <div> + <label class="block text-sm mb-2 dark:text-white">Description</label> + <textarea v-model="description" rows="3" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" /> + </div> + + <div> + <label class="block text-sm mb-2 dark:text-white">New Password</label> + <input type="password" v-model="newPassword" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" /> + <p v-if="newPassword && passwordError" class="text-red-500 text-sm mt-1">{{ passwordError }}</p> + </div> + + <button + type="submit" + :disabled="loading || !!passwordError" + class="w-full py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50" + > + <span v-if="!loading">Save Changes</span> + <span v-else>Saving...</span> + </button> + </form> + </div> + + <!-- Tabs --> + <div class="flex justify-center gap-4 mb-6"> + <button + v-for="tab in ['watchlist', 'followers', 'following', 'reviews']" + :key="tab" + @click="activeTab = tab" + :class="[ + 'px-4 py-2 rounded-lg text-sm font-medium', + activeTab === tab + ? 'bg-blue-600 text-white' + : 'bg-white dark:bg-neutral-700 text-gray-800 dark:text-white border border-gray-300 dark:border-neutral-600' + ]" + > + {{ tab.charAt(0).toUpperCase() + tab.slice(1) }} + </button> + </div> + + <!-- Tab Content --> + <div v-if="activeTab === 'watchlist'" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> + <router-link + v-for="movie in watchlist" + :key="movie.id" + :to="`/films/${movie.id}`" + class="block bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg p-3 hover:opacity-90 transition" + > + <img + v-if="movie.poster_path" + :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path" + alt="Poster" + class="w-full h-60 object-cover rounded mb-3" + /> + <p class="font-semibold text-gray-800 dark:text-white">{{ movie.title }}</p> + <p class="text-sm text-gray-600 dark:text-neutral-400">â {{ movie.vote_average }} — {{ movie.status }}</p> + </router-link> + </div> + + <!-- Followers --> + <div v-else-if="activeTab === 'followers'" class="grid gap-4 max-w-xl mx-auto"> + <div + v-for="follower in followers" + :key="follower.uid" + class="flex items-center justify-between p-4 bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg" + > + <router-link :to="`/user/${follower.uid}`" class="flex items-center gap-4"> + <img + :src="follower.photoURL || 'https://via.placeholder.com/150'" + alt="Avatar" + class="w-12 h-12 rounded-full object-cover" + /> + <span class="text-lg font-semibold text-gray-800 dark:text-white">{{ follower.displayName }}</span> + </router-link> + </div> + </div> + + <!-- Following --> + <div v-else-if="activeTab === 'following'" class="grid gap-4 max-w-xl mx-auto"> + <div + v-for="followed in following" + :key="followed.uid" + class="flex items-center justify-between p-4 bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg" + > + <router-link :to="`/user/${followed.uid}`" class="flex items-center gap-4"> + <img + :src="followed.photoURL || 'https://via.placeholder.com/150'" + alt="Avatar" + class="w-12 h-12 rounded-full object-cover" + /> + <span class="text-lg font-semibold text-gray-800 dark:text-white">{{ followed.displayName }}</span> + </router-link> + </div> + </div> + + <!-- Reviews --> + <div v-else-if="activeTab === 'reviews'" class="max-w-2xl mx-auto space-y-6"> + <div v-if="reviewsLoading" class="text-gray-600 dark:text-gray-400">Loading reviews…</div> + <div v-else-if="!reviews.length" class="text-gray-600 dark:text-gray-400">No reviews yet.</div> + <div v-else class="space-y-4"> + <div v-for="r in reviews" :key="r.id" class="bg-white dark:bg-neutral-800 p-4 rounded-lg border dark:border-neutral-700 text-left"> + <router-link :to="`/films/${r.movieId}`" class="font-semibold text-lg text-blue-600 dark:text-blue-400 hover:underline"> + {{ r.movieTitle }} + </router-link> + <div class="mt-1 text-sm text-gray-800 dark:text-gray-200">â {{ r.rating }}</div> + <p class="mt-2 text-gray-700 dark:text-neutral-300">{{ r.text }}</p> + </div> + </div> + </div> + </div> +</template> + +<script setup> +import { + collection, + getDocs, + doc, + getDoc, + updateDoc, + getFirestore, +} from 'firebase/firestore' + +import { + getAuth, + updateProfile as firebaseUpdateProfile, + updatePassword +} from 'firebase/auth' + +import { ref, computed, onMounted } from 'vue' + +const TMDB_API_KEY = import.meta.env.VITE_TMDB_API_KEY + +const auth = getAuth() +const db = getFirestore() + +const user = ref(null) +const userEmail = ref('') +const displayName = ref('') +const description = ref('') +const userPhotoURL = ref('') +const newPassword = ref('') +const showForm = ref(false) +const activeTab = ref('watchlist') +const loading = ref(false) + +const watchlist = ref([]) +const followers = ref([]) +const following = ref([]) +const reviews = ref([]) +const reviewsLoading = ref(true) + +const passwordError = computed(() => { + if (!newPassword.value) return '' + if (newPassword.value.length < 8) return 'Password must be at least 8 characters.' + if (!/[A-Z]/.test(newPassword.value)) return 'Must include at least 1 uppercase letter.' + if (!/[0-9]/.test(newPassword.value)) return 'Must include at least 1 number.' + if (!/[@$!%*?&]/.test(newPassword.value)) return 'Must include at least 1 special character (@$!%*?&).' + return '' +}) + +const fetchFollowing = async () => { + const followingSnap = await getDocs(collection(db, 'users', user.value.uid, 'following')) + const followedIds = followingSnap.docs.map(doc => doc.id) + + const usersList = [] + + for (const id of followedIds) { + const docSnap = await getDoc(doc(db, 'users', id)) + if (docSnap.exists()) { + const data = docSnap.data() + // Only push if the user has a displayName or isn’t hidden + if (!data.hidden && data.displayName) { + usersList.push({ uid: id, ...data }) + } + } + } + + following.value = usersList +} + +const fetchFollowers = async () => { + if (!user.value) return + + const allUsersSnap = await getDocs(collection(db, 'users')) + const followersList = [] + + for (const docSnap of allUsersSnap.docs) { + const otherUid = docSnap.id + if (otherUid === user.value.uid) continue + + const followDocRef = doc(db, 'users', otherUid, 'following', user.value.uid) + const followDocSnap = await getDoc(followDocRef) + + if (followDocSnap.exists()) { + const otherUserData = docSnap.data() + if (otherUserData.displayName && !otherUserData.hidden) { + followersList.push({ uid: otherUid, ...otherUserData }) + } + } + } + + followers.value = followersList +} + +const fetchReviews = async () => { + const uid = user.value.uid + const snap = await getDocs(collection(db, 'users', uid, 'reviews')) + const revDocs = snap.docs.map(d => ({ id: d.id, ...d.data() })) + + // Fetch all movie titles in parallel + const withTitles = await Promise.all( + revDocs.map(async r => { + // Call TMDB for each movieId + const res = await fetch( + `https://api.themoviedb.org/3/movie/${r.movieId}?api_key=${TMDB_API_KEY}` + ) + const movieData = await res.json() + return { + id: r.id, + movieId: r.movieId, + movieTitle: movieData.title, + rating: r.rating, + text: r.text, + } + }) + ) + + reviews.value = withTitles + reviewsLoading.value = false +} + +const handleUpdateProfile = async () => { + if (passwordError.value) return + + loading.value = true + try { + const authUser = auth.currentUser + if (!authUser) throw new Error('No authenticated user') + + // Update the Firebase Auth profile + await firebaseUpdateProfile(authUser, { + displayName: displayName.value + }) + + if (newPassword.value) { + await updatePassword(authUser, newPassword.value) + } + + // Update your Firestore user document + const userDocRef = doc(db, 'users', authUser.uid) + await updateDoc(userDocRef, { + displayName: displayName.value, + description: description.value + }) + + alert('Profile updated successfully!') + showForm.value = false + + } catch (error) { + alert(error.message) + } finally { + loading.value = false + } +} + +onMounted(async () => { + const authUser = auth.currentUser + if (!authUser) return + + user.value = authUser + userEmail.value = authUser.email + displayName.value = authUser.displayName || '' + userPhotoURL.value = authUser.photoURL || '' + + const userDoc = await getDoc(doc(db, 'users', user.value.uid)) + if (userDoc.exists()) { + description.value = userDoc.data().description || '' + } + + const snapshot = await getDocs(collection(db, 'users', user.value.uid, 'watchlist')) + watchlist.value = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) + + await fetchFollowing() + await fetchFollowers() + await fetchReviews() +}) +</script> + +<style scoped> +input:disabled { + cursor: not-allowed; +} +</style> \ No newline at end of file diff --git a/movie-group-8/src/views/RecoverAccount.vue b/movie-group-8/src/views/RecoverAccount.vue new file mode 100644 index 0000000000000000000000000000000000000000..409f2f1d58396b37b9e8881937ccb84ec23d2a38 --- /dev/null +++ b/movie-group-8/src/views/RecoverAccount.vue @@ -0,0 +1 @@ +<template>Recover Account</template> \ No newline at end of file diff --git a/movie-group-8/src/views/Register.vue b/movie-group-8/src/views/Register.vue new file mode 100644 index 0000000000000000000000000000000000000000..1f4d10208d7c4e78d2394a6ef467358234f9f5f4 --- /dev/null +++ b/movie-group-8/src/views/Register.vue @@ -0,0 +1,132 @@ +<template> + <div class="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-neutral-900"> + <div class="mt-7 bg-white border border-gray-200 rounded-xl shadow-2xs dark:bg-neutral-900 dark:border-neutral-700 min-w-96"> + <div class="p-4 sm:p-7"> + <div class="text-center"> + <h1 class="block text-2xl font-bold text-gray-800 dark:text-white">Register</h1> + <p class="mt-2 text-sm text-gray-600 dark:text-neutral-400"> + Already have an account? + <a class="text-blue-600 decoration-2 hover:underline font-medium dark:text-blue-500" href="/login"> + Sign in here + </a> + </p> + </div> + + <div class="mt-5"> + <button @click="signInWithGoogle" type="button" class="w-full py-3 px-4 flex justify-center items-center gap-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800"> + <svg class="w-4 h-auto" width="46" height="47" viewBox="0 0 46 47" fill="none"> + <path d="M46 24.0287C46 22.09 45.8533 20.68 45.5013 19.2112H23.4694V27.9356H36.4069C36.1429 30.1094 34.7347 33.37 31.5957 35.5731L31.5663 35.8669L38.5191 41.2719L38.9885 41.3306C43.4477 37.2181 46 31.1669 46 24.0287Z" fill="#4285F4"/> + <path d="M23.4694 47C29.8061 47 35.1161 44.9144 39.0179 41.3012L31.625 35.5437C29.6301 36.9244 26.9898 37.8937 23.4987 37.8937C17.2793 37.8937 12.0281 33.7812 10.1505 28.1412L9.88649 28.1706L2.61097 33.7812L2.52296 34.0456C6.36608 41.7125 14.287 47 23.4694 47Z" fill="#34A853"/> + <path d="M10.1212 28.1413C9.62245 26.6725 9.32908 25.1156 9.32908 23.5C9.32908 21.8844 9.62245 20.3275 10.0918 18.8588V18.5356L2.75765 12.8369L2.52296 12.9544C0.909439 16.1269 0 19.7106 0 23.5C0 27.2894 0.909439 30.8731 2.49362 34.0456L10.1212 28.1413Z" fill="#FBBC05"/> + <path d="M23.4694 9.07688C27.8699 9.07688 30.8622 10.9863 32.5344 12.5725L39.1645 6.11C35.0867 2.32063 29.8061 0 23.4694 0C14.287 0 6.36607 5.2875 2.49362 12.9544L10.0918 18.8588C11.9987 13.1894 17.25 9.07688 23.4694 9.07688Z" fill="#EB4335"/> + </svg> + Sign up with Google + </button> + + <div class="py-3 flex items-center text-xs text-gray-400 uppercase before:flex-1 before:border-t before:border-gray-200 after:flex-1 after:border-t after:border-gray-200 dark:text-neutral-500 dark:before:border-neutral-600 dark:after:border-neutral-600">Or</div> + + <form @submit.prevent="register"> + <div class="grid gap-y-4"> + <div> + <label class="block text-sm mb-2 dark:text-white">Email address</label> + <input type="email" v-model="email" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required /> + </div> + + <div> + <label class="block text-sm mb-2 dark:text-white">Password</label> + <input type="password" v-model="password" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required /> + <p v-if="password && passwordError" class="text-red-500 text-sm mt-1">{{ passwordError }}</p> + </div> + + <div> + <label class="block text-sm mb-2 dark:text-white">Confirm Password</label> + <input type="password" v-model="confirmPassword" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required /> + <p v-if="confirmPassword && confirmPasswordError" class="text-red-500 text-sm mt-1">{{ confirmPasswordError }}</p> + </div> + + <button + type="submit" + :disabled="loading || !!passwordError || !!confirmPasswordError" + class="w-full py-3 bg-blue-600 text-white rounded-lg flex justify-center items-center gap-2 hover:bg-blue-700 transition duration-300 disabled:opacity-50 disabled:pointer-events-none" + > + <span v-if="!loading">Sign up</span> + <span v-else>Loading...</span> + </button> + </div> + </form> + </div> + </div> + </div> + </div> +</template> + +<script setup> +import { ref, computed } from 'vue'; +import { useRouter } from 'vue-router'; +import defaultAvatar from '@/assets/default_avatar.png' + +import { + getAuth, + createUserWithEmailAndPassword, + } from 'firebase/auth'; +import { + getFirestore, + doc, + setDoc +} from 'firebase/firestore'; + +const email = ref(''); +const password = ref(''); +const confirmPassword = ref(''); +const router = useRouter(); +const loading = ref(false); + +// Password validation +const passwordError = computed(() => { +if (password.value.length < 8) return "Password must be at least 8 characters."; +if (!/[A-Z]/.test(password.value)) return "Must include at least 1 uppercase letter."; +if (!/[0-9]/.test(password.value)) return "Must include at least 1 number."; +if (!/[@$!%*?&]/.test(password.value)) return "Must include at least 1 special character (@$!%*?&)."; +return ""; +}); + +// Confirm password validation +const confirmPasswordError = computed(() => { +if (confirmPassword.value && confirmPassword.value !== password.value) { + return "Passwords do not match."; +} +return ""; +}); + +// Register function +const register = async () => { +if (passwordError.value || confirmPasswordError.value) return; +loading.value = true; +try { + const auth = getAuth() + const db = getFirestore() + const userCredential = await createUserWithEmailAndPassword(auth, email.value, password.value) + const user = userCredential.user + + // Set default profile info + await updateProfile(user, { + displayName: 'New User', + photoURL: defaultAvatar + }) + + // Create Firestore document for this user + await setDoc(doc(db, 'users', user.uid), { + displayName: 'New User', + photoURL: defaultAvatar, + description: '', + hidden: false + }) + + router.push('/films') +} catch (error) { + alert(error.message); +} finally { + loading.value = false; +} +}; +</script> diff --git a/movie-group-8/src/views/ReviewPage.vue b/movie-group-8/src/views/ReviewPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..7ea921a2631c922299401a5883b07c1a71d559f7 --- /dev/null +++ b/movie-group-8/src/views/ReviewPage.vue @@ -0,0 +1,218 @@ +<template> + <div class="review-page"> + <!-- Back --> + <button class="back-button" @click="$router.back()">↠Back</button> + + <!-- Loading / Error states --> + <div v-if="loading" class="loading">Loading movie...</div> + <div v-else-if="error" class="error">Error: {{ error }}</div> + + <!-- Main content --> + <div v-else class="content"> + <!-- Poster & Title --> + <div class="header"> + <img + v-if="movie.poster_path" + :src="`https://image.tmdb.org/t/p/w300${movie.poster_path}`" + :alt="movie.title + ' poster'" + class="poster" + /> + <h1 class="title">{{ movie.title }}</h1> + </div> + + <!-- Star Rating --> + <div class="rating"> + <span + v-for="star in 5" + :key="star" + class="star" + :class="{ filled: star <= userRating }" + @click="setRating(star)" + > + ★ + </span> + <span class="rating-value">{{ userRating }} / 5</span> + </div> + + <!-- Review Text --> + <textarea + v-model="reviewText" + placeholder="Write your review…" + class="review-text" + rows="6" + ></textarea> + + <!-- Submit --> + <button class="submit-btn" @click="handleSubmit"> + Submit Review + </button> + </div> + </div> + </template> + + <script setup> + import { ref, onMounted } from 'vue' + import { useRoute, useRouter } from 'vue-router' + import { getUserReview, submitReview } from '@/composables/useReviews.js' + + const route = useRoute() + const router = useRouter() + + // State + const movie = ref(null) + const loading = ref(true) + const error = ref('') + const userRating = ref(0) + const reviewText = ref('') + + // Fetch movie details + async function fetchMovie() { + try { + const id = route.params.id + const res = await fetch( + `${import.meta.env.VITE_TMDB_BASE}/movie/${id}?api_key=${import.meta.env.VITE_TMDB_API_KEY}` + ) + if (!res.ok) throw new Error('Failed to load movie') + movie.value = await res.json() + } catch (err) { + error.value = err.message + } + } + + // Load existing user review + async function loadExistingReview() { + try { + const existing = await getUserReview(route.params.id) + if (existing) { + userRating.value = existing.rating || 0 + reviewText.value = existing.text || '' + } + } catch (err) { + console.error('Error loading review:', err) + } + } + + // Rating handler + function setRating(star) { + userRating.value = star + } + + // Submit handler + async function handleSubmit() { + try { + await submitReview({ + movieId: route.params.id, + rating: userRating.value, + text: reviewText.value + }) + router.back() + } catch (err) { + console.error('Error submitting review:', err) + error.value = err.message + } + } + + onMounted(async () => { + await fetchMovie() + await loadExistingReview() + loading.value = false + }) + </script> + + <style scoped> + .review-page { + background: #121212; + color: #fff; + min-height: 100vh; + padding: 2rem; + box-sizing: border-box; + } + + .back-button { + background: transparent; + border: none; + color: #fff; + font-size: 1rem; + } + + .loading, + .error { + text-align: center; + margin: 2rem 0; + } + + .content { + max-width: 600px; + margin: 2rem auto; + background: #1e1e1e; + padding: 2rem; + border-radius: 8px; + } + + .header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; + } + + .poster { + width: 100px; + border-radius: 4px; + } + + .title { + font-size: 1.75rem; + margin: 0; + } + + .rating { + display: flex; + align-items: center; + margin-bottom: 1rem; + } + + .star { + font-size: 2rem; + cursor: pointer; + transition: transform 0.1s; + margin-right: 0.25rem; + color: #555; + } + .star.filled { + color: #f5c518; + } + .star:hover { + transform: scale(1.2); + } + + .rating-value { + margin-left: 0.5rem; + color: #bbb; + } + + .review-text { + width: 100%; + padding: 0.75rem; + border: none; + border-radius: 4px; + background: #2a2a2a; + color: #fff; + margin-bottom: 1rem; + resize: vertical; + } + + .submit-btn { + background: #1976d2; + border: none; + color: #fff; + padding: 0.75rem 1.5rem; + border-radius: 4px; + cursor: pointer; + width: 100%; + } + .submit-btn:hover { + background: #1565c0; + } + </style> + \ No newline at end of file diff --git a/movie-group-8/src/views/Settings.vue b/movie-group-8/src/views/Settings.vue new file mode 100644 index 0000000000000000000000000000000000000000..4c5d407983bd7e51dd0c6b32680470d99bd2029a --- /dev/null +++ b/movie-group-8/src/views/Settings.vue @@ -0,0 +1 @@ +<template>Settings</template> \ No newline at end of file diff --git a/movie-group-8/src/views/Social.vue b/movie-group-8/src/views/Social.vue new file mode 100644 index 0000000000000000000000000000000000000000..ed3b9b1e253cf69fa6e2c191f9b1ebea19fcb48f --- /dev/null +++ b/movie-group-8/src/views/Social.vue @@ -0,0 +1,99 @@ +<template> + <div class="min-h-screen bg-gray-100 dark:bg-neutral-900 p-6"> + <h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-6 text-center">Find and Follow Users</h1> + + <div class="max-w-xl mx-auto mb-6"> + <input + type="text" + v-model="searchTerm" + placeholder="Search by display name..." + class="w-full p-3 rounded-lg border border-gray-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white" + /> + </div> + + <div class="grid gap-4 max-w-3xl mx-auto"> + <div + v-for="user in filteredUsers" + :key="user.uid" + class="flex items-center justify-between p-4 bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg" + > + <router-link :to="`/user/${user.uid}`" class="flex items-center gap-4"> + <img + :src="user.photoURL" + class="w-12 h-12 rounded-full object-cover" + alt="Avatar" + /> + <span class="text-lg font-semibold text-gray-800 dark:text-white">{{ user.displayName }}</span> + </router-link> + <button + @click="toggleFollow(user.uid)" + class="px-4 py-1.5 text-sm rounded-lg font-medium" + :class="isFollowing(user.uid) + ? 'bg-red-500 text-white hover:bg-red-600' + : 'bg-blue-600 text-white hover:bg-blue-700'" + > + {{ isFollowing(user.uid) ? 'Unfollow' : 'Follow' }} + </button> + </div> + </div> + </div> +</template> + +<script setup> +import { ref, computed, onMounted } from 'vue' +import { getAuth } from 'firebase/auth' +import { + getFirestore, + collection, + getDocs, + setDoc, + deleteDoc, + doc +} from 'firebase/firestore' + +const auth = getAuth() +const db = getFirestore() +const currentUser = auth.currentUser + +const searchTerm = ref('') +const users = ref([]) +const following = ref([]) + +onMounted(async () => { + await fetchUsers() + await fetchFollowing() +}) + +const fetchUsers = async () => { + const snapshot = await getDocs(collection(db, 'users')) + users.value = snapshot.docs + .map(doc => doc.data()) + .filter(u => u.uid !== currentUser?.uid && !u.hidden) // exclude self and test user +} + +const fetchFollowing = async () => { + const snapshot = await getDocs(collection(db, 'users', currentUser.uid, 'following')) + following.value = snapshot.docs.map(doc => doc.id) +} + +const isFollowing = (uid) => following.value.includes(uid) + +const toggleFollow = async (uid) => { + const followRef = doc(db, 'users', currentUser.uid, 'following', uid) + + if (isFollowing(uid)) { + await deleteDoc(followRef) + following.value = following.value.filter(id => id !== uid) + } else { + await setDoc(followRef, { followedAt: Date.now() }) + following.value.push(uid) + } +} + +const filteredUsers = computed(() => { + if (!searchTerm.value.trim()) return users.value + return users.value.filter(u => + u.displayName.toLowerCase().includes(searchTerm.value.toLowerCase()) + ) +}) +</script> \ No newline at end of file diff --git a/movie-group-8/src/views/TopRated.vue b/movie-group-8/src/views/TopRated.vue new file mode 100644 index 0000000000000000000000000000000000000000..a8e7c29534e976e832e311de90f8230cd613b57a --- /dev/null +++ b/movie-group-8/src/views/TopRated.vue @@ -0,0 +1,44 @@ +<script setup> +import TopRatedMovies from '@/components/TopRatedMovies.vue' + +</script> + +<template> + <TopRatedMovies /> + <template> + <div class="nyt-header"> + <h1>The Movies We've Loved Since 2000</h1> + <p> + Explore top-rated films using the filters below — by genre and year — and rediscover hidden gems. + </p> + </div> + + <div class="filters"> + <select v-model="genre"> + <option value="">Pick a genre ...</option> + <option v-for="g in genres" :key="g.id" :value="g.id">{{ g.name }}</option> + </select> + + <select v-model="year"> + <option v-for="y in Array.from({length: 25}, (_, i) => 2000 + i)" :key="y" :value="y"> + {{ y }} + </option> + </select> + </div> + + <h2 class="section-title">Our favorite movies from {{ year }}</h2> + + <div class="movie-grid"> + <div v-for="movie in movies" :key="movie.id" class="movie-card"> + <img + v-if="movie.poster_path" + :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path" + alt="poster" + /> + <p class="title">{{ movie.title }}</p> + <p class="rating">â {{ movie.vote_average }}</p> + </div> + </div> +</template> + +</template> diff --git a/movie-group-8/src/views/UserProfile.vue b/movie-group-8/src/views/UserProfile.vue new file mode 100644 index 0000000000000000000000000000000000000000..e3b650ebab1cd98051851e8b453fffa22cf2fe62 --- /dev/null +++ b/movie-group-8/src/views/UserProfile.vue @@ -0,0 +1,190 @@ +<template> + <div class="min-h-screen bg-gray-100 dark:bg-neutral-900 p-6"> + <div class="text-center mb-10"> + <img + v-if="user.photoURL" + :src="user.photoURL" + alt="Profile" + class="w-24 h-24 mx-auto rounded-full object-cover" + /> + <h1 class="text-2xl font-bold mt-4 text-gray-800 dark:text-white"> + {{ user.displayName || 'Unknown User' }} + </h1> + + <button + v-if="!isOwnProfile" + @click="toggleFollow" + class="mt-3 px-4 py-2 rounded-lg text-white text-sm" + :class="isFollowing ? 'bg-red-500 hover:bg-red-600' : 'bg-blue-600 hover:bg-blue-700'" + > + {{ isFollowing ? 'Unfollow' : 'Follow' }} + </button> + </div> + + <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4 text-center"> + Watchlist + </h2> + + <div v-if="watchlist.length === 0" class="text-center text-gray-500 dark:text-gray-400"> + No public watchlist found. + </div> + + <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mb-12"> + <router-link + v-for="movie in watchlist" + :key="movie.id" + :to="`/films/${movie.id}`" + class="block bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg p-4 hover:opacity-90 transition" + > + <img + v-if="movie.poster_path" + :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path" + alt="Poster" + class="w-full h-60 object-cover rounded mb-2" + /> + <p class="font-semibold text-gray-800 dark:text-white">{{ movie.title }}</p> + <p class="text-sm text-gray-600 dark:text-neutral-400">â {{ movie.vote_average }}</p> + <p class="text-xs text-gray-400 dark:text-neutral-500">{{ movie.status }}</p> + </router-link> + </div> + + <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4 text-center"> + Reviews + </h2> + <div v-if="reviewsLoading" class="text-center text-gray-500 dark:text-gray-400"> + Loading reviews… + </div> + <div v-else-if="!reviews.length" class="text-gray-400"> + No reviews yet. + </div> + <div class="space-y-6 max-w-3xl mx-auto"> + <div + v-for="r in reviews" + :key="r.id" + class="bg-white dark:bg-neutral-800 p-4 rounded-lg border dark:border-neutral-700 text-left" + > + <router-link + :to="`/films/${r.movieId}`" + class="block text-lg font-semibold text-blue-600 dark:text-blue-400 hover:underline" + > + {{ r.movieTitle }} + </router-link> + <div class="mt-1 text-sm text-gray-800 dark:text-gray-200">â {{ r.rating }}</div> + <p class="mt-2 text-gray-700 dark:text-neutral-300">{{ r.text }}</p> + </div> + </div> + </div> +</template> + +<script setup> +import { ref, onMounted } from 'vue' +import { useRoute } from 'vue-router' +import { getAuth, onAuthStateChanged } from 'firebase/auth' +import { + getFirestore, + doc, + getDoc, + collection, + getDocs, + setDoc, + deleteDoc +} from 'firebase/firestore' + +const route = useRoute() +const db = getFirestore() +const auth = getAuth() + +const profileUserId = route.params.id + +const user = ref({}) +const watchlist = ref([]) +const isFollowing = ref(false) +const isOwnProfile = ref(false) +const TMDB_API_KEY = import.meta.env.VITE_TMDB_API_KEY +const reviews = ref([]) +const reviewsLoading = ref(true) + +onMounted(async () => { + onAuthStateChanged(auth, async (u) => { + if (!u) { + // not logged in → just show public data + await fetchUserProfile() + await fetchWatchlist() + await fetchReviews() + return + } + + // now we have a valid user + isOwnProfile.value = (u.uid === profileUserId) + await fetchUserProfile() + await fetchWatchlist() + if (!isOwnProfile.value) { + await checkIfFollowing(u.uid) + } + await fetchReviews() + }) +}) + +const fetchUserProfile = async () => { + const docRef = doc(db, 'users', profileUserId) + const docSnap = await getDoc(docRef) + if (docSnap.exists()) { + user.value = docSnap.data() + } else { + user.value = { displayName: 'User Not Found' } + } +} + +async function fetchReviews() { + const snap = await getDocs(collection(db, 'users', profileUserId, 'reviews')) + const revs = snap.docs.map(d => ({ + id: d.id, + ...d.data(), + movieId: d.data().movieId, + text: d.data().text, + rating: d.data().rating, + })) + + // lookup titles in parallel + const withTitles = await Promise.all( + revs.map(async r => { + const res = await fetch( + `https://api.themoviedb.org/3/movie/${r.movieId}?api_key=${TMDB_API_KEY}` + ) + const md = await res.json() + return { + ...r, + movieTitle: md.title + } + }) + ) + + reviews.value = withTitles + reviewsLoading.value = false +} + +const fetchWatchlist = async () => { + const snapshot = await getDocs(collection(db, 'users', profileUserId, 'watchlist')) + watchlist.value = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) +} + +async function checkIfFollowing(currentUid) { + const followRef = doc(db, 'users', currentUid, 'following', profileUserId) + const followSnap = await getDoc(followRef) + isFollowing.value = followSnap.exists() +} + +const toggleFollow = async () => { + const u = auth.currentUser + if (!u) return + const followRef = doc(db, 'users', u.uid, 'following', profileUserId) + + if (isFollowing.value) { + await deleteDoc(followRef) + isFollowing.value = false + } else { + await setDoc(followRef, { followedAt: Date.now() }) + isFollowing.value = true + } +} +</script> \ No newline at end of file diff --git a/movie-group-8/src/views/WatchlistView.vue b/movie-group-8/src/views/WatchlistView.vue new file mode 100644 index 0000000000000000000000000000000000000000..071b02ebe222a37ce5d458cfbc3781c56c890380 --- /dev/null +++ b/movie-group-8/src/views/WatchlistView.vue @@ -0,0 +1,95 @@ +<template> + <div class="min-h-screen bg-gray-100 dark:bg-neutral-900 p-6"> + <h1 class="text-3xl font-bold mb-6 text-gray-900 dark:text-white text-center">Your Watchlist</h1> + + <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> + <!-- Planned Movies --> + <div> + <h2 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">📌 Planned to Watch</h2> + <div v-if="plannedMovies.length === 0" class="text-gray-500 dark:text-gray-400 text-sm"> + No movies planned yet. + </div> + <div class="grid gap-4"> + <div v-for="movie in plannedMovies" :key="movie.id" class="relative bg-white dark:bg-neutral-800 p-4 rounded"> + <MovieCard + :movie="movie" + @status-changed="updateMovieStatus" + /> + <button + @click="removeFromWatchlist(movie.id)" + class="absolute top-2 right-2 px-2 py-1 bg-red-600 text-white text-xs rounded hover:bg-red-700 transition" + > + Remove + </button> + </div> + </div> + </div> + + <!-- Watched Movies --> + <div> + <h2 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">✅ Watched</h2> + <div v-if="watchedMovies.length === 0" class="text-gray-500 dark:text-gray-400 text-sm"> + No watched movies yet. + </div> + <div class="grid gap-4"> + <div v-for="movie in watchedMovies" :key="movie.id" class="relative bg-white dark:bg-neutral-800 p-4 rounded"> + <MovieCard + :movie="movie" + @status-changed="updateMovieStatus" + /> + <button + @click="removeFromWatchlist(movie.id)" + class="absolute top-2 right-2 px-2 py-1 bg-red-600 text-white text-xs rounded hover:bg-red-700 transition" + > + Remove + </button> + </div> + </div> + </div> + </div> + </div> +</template> + +<script setup> +import { ref, onMounted } from 'vue' +import { getAuth } from 'firebase/auth' +import { getFirestore, collection, getDocs, doc, updateDoc, deleteDoc } from 'firebase/firestore' +import MovieCard from '@/components/MovieCard.vue' + +const auth = getAuth() +const db = getFirestore() + +const plannedMovies = ref([]) +const watchedMovies = ref([]) + +const fetchWatchlist = async () => { + const user = auth.currentUser + if (!user) return + + const snapshot = await getDocs(collection(db, 'users', user.uid, 'watchlist')) + const all = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) + plannedMovies.value = all.filter(m => m.status === 'planned') + watchedMovies.value = all.filter(m => m.status === 'watched') +} + +const removeFromWatchlist = async (movieId) => { + const user = auth.currentUser + if (!user) return + + const movieRef = doc(db, 'users', user.uid, 'watchlist', movieId) + await deleteDoc(movieRef) + await fetchWatchlist() +} + +const updateMovieStatus = async (movieId, newStatus) => { + const user = auth.currentUser + if (!user) return + + const movieRef = doc(db, 'users', user.uid, 'watchlist', movieId) + await updateDoc(movieRef, { status: newStatus }) + + await fetchWatchlist() // Refresh lists +} + +onMounted(fetchWatchlist) +</script> diff --git a/movie-group-8/vite.config.js b/movie-group-8/vite.config.js new file mode 100644 index 0000000000000000000000000000000000000000..8bbf4703424d6582b10489e90bdad6599bfa8345 --- /dev/null +++ b/movie-group-8/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' +import path from 'path' + + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue(), + tailwindcss() + ], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + +})