diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e6c26a2 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,58 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ['main'] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: 'pages' + cancel-in-progress: true + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Create 404.html + run: | + cp dist/index.html dist/404.html + - name: Rename dist to docs + run: mv dist docs + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload docs repository + path: './docs' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + with: + target_folder: docs \ No newline at end of file diff --git a/angular.json b/angular.json index e496ce8..5792ce6 100644 --- a/angular.json +++ b/angular.json @@ -50,7 +50,13 @@ "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true + "sourceMap": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ] } }, "defaultConfiguration": "production" @@ -92,5 +98,8 @@ } } } + }, + "cli": { + "analytics": "e6f8a3cf-426a-4970-95b0-a18c7458838e" } } diff --git a/package-lock.json b/package-lock.json index 3bc774c..f785955 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@angular/platform-browser": "^19.1.0", "@angular/platform-browser-dynamic": "^19.1.0", "@angular/router": "^19.1.0", + "ngx-infinite-scroll": "^19.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -25,15 +26,31 @@ "@angular/cli": "^19.1.3", "@angular/compiler-cli": "^19.1.0", "@types/jasmine": "~5.1.0", + "autoprefixer": "^10.4.20", "jasmine-core": "~5.5.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", "typescript": "~5.7.2" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -190,6 +207,35 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/@angular-devkit/build-webpack": { "version": "0.1901.3", "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1901.3.tgz", @@ -5454,6 +5500,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -5481,6 +5534,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6051,6 +6111,16 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001695", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", @@ -6835,6 +6905,20 @@ "dev": true, "license": "MIT" }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -9409,6 +9493,19 @@ } } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -10319,6 +10416,18 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -10387,6 +10496,19 @@ "dev": true, "license": "MIT" }, + "node_modules/ngx-infinite-scroll": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-19.0.0.tgz", + "integrity": "sha512-Ft4xNNDLXoDGi2hF6ylehjxbG8JIgfoL6qDWWcebGMcbh1CEfEsh0HGkDuFlX/cBBMenRh2HFbXlYq8BAtbvLw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=19.0.0 <20.0.0", + "@angular/core": ">=19.0.0 <20.0.0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -10740,6 +10862,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -11244,6 +11376,16 @@ "node": ">=6" } }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/piscina": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", @@ -11271,9 +11413,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "dev": true, "funding": [ { @@ -11291,7 +11433,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11299,6 +11441,80 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, "node_modules/postcss-loader": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", @@ -11401,6 +11617,46 @@ "postcss": "^8.1.0" } }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-selector-parser": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", @@ -11582,6 +11838,26 @@ "node": ">= 0.8" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -12987,6 +13263,86 @@ "node": ">=8" } }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13023,6 +13379,122 @@ "node": ">=0.10" } }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -13188,6 +13660,29 @@ } } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/thingies": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", @@ -13271,6 +13766,13 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -14252,6 +14754,19 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 9dd9e02..20d383a 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@angular/platform-browser": "^19.1.0", "@angular/platform-browser-dynamic": "^19.1.0", "@angular/router": "^19.1.0", + "ngx-infinite-scroll": "^19.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -27,12 +28,15 @@ "@angular/cli": "^19.1.3", "@angular/compiler-cli": "^19.1.0", "@types/jasmine": "~5.1.0", + "autoprefixer": "^10.4.20", "jasmine-core": "~5.5.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", "typescript": "~5.7.2" } } diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..c35ff83 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index ad4f65a..f08feb1 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,336 +1,5 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - - +
+ + + +
\ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 36d5a50..52f7e36 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,9 +1,12 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { NavbarComponent } from './components/navbar/navbar.component'; +import { FooterComponent } from './components/footer/footer.component'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + standalone: true, + imports: [RouterOutlet, NavbarComponent, FooterComponent], templateUrl: './app.component.html', styleUrl: './app.component.css' }) diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 460a570..46b36e0 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -2,7 +2,8 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; +import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)] + providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient()], }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 4f3af40..f845467 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,20 @@ import { Routes } from '@angular/router'; +import { HomeComponent } from './pages/home/home.component'; +import { MoviesComponent } from './pages/movies/movies.component'; +import { ShowMovieComponent } from './pages/show-movie/show-movie.component'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: '', + component: HomeComponent, + pathMatch: 'full', + }, + { + path: 'movies', + component: MoviesComponent, + }, + { + path: 'show-movie/:movieId', + component: ShowMovieComponent, + }, +]; diff --git a/src/app/components/footer/footer.component.css b/src/app/components/footer/footer.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/footer/footer.component.html b/src/app/components/footer/footer.component.html new file mode 100644 index 0000000..83b5c63 --- /dev/null +++ b/src/app/components/footer/footer.component.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/app/components/footer/footer.component.spec.ts b/src/app/components/footer/footer.component.spec.ts new file mode 100644 index 0000000..642a81c --- /dev/null +++ b/src/app/components/footer/footer.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FooterComponent } from './footer.component'; + +describe('FooterComponent', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FooterComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/footer/footer.component.ts b/src/app/components/footer/footer.component.ts new file mode 100644 index 0000000..e1baaf0 --- /dev/null +++ b/src/app/components/footer/footer.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-footer', + imports: [], + templateUrl: './footer.component.html', + styleUrl: './footer.component.css' +}) +export class FooterComponent { + +} diff --git a/src/app/components/movie/movie.component.css b/src/app/components/movie/movie.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/movie/movie.component.html b/src/app/components/movie/movie.component.html new file mode 100644 index 0000000..912e3d3 --- /dev/null +++ b/src/app/components/movie/movie.component.html @@ -0,0 +1,24 @@ + + @if(movie.poster_path){ + + } + +
+

+ {{ movie.title }} +

+ +
+

+ {{ movie.release_date | date }} +

+

+ Rating: {{ movie.vote_average }} +

+
+
+
\ No newline at end of file diff --git a/src/app/components/movie/movie.component.spec.ts b/src/app/components/movie/movie.component.spec.ts new file mode 100644 index 0000000..ad81295 --- /dev/null +++ b/src/app/components/movie/movie.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MovieComponent } from './movie.component'; + +describe('MovieComponent', () => { + let component: MovieComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MovieComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MovieComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/movie/movie.component.ts b/src/app/components/movie/movie.component.ts new file mode 100644 index 0000000..85bd3ed --- /dev/null +++ b/src/app/components/movie/movie.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { imagesBaseUrl } from '../../services/movies.service'; +import { Movie } from '../../models/movie'; +import { RouterModule } from '@angular/router'; +import { DatePipe } from '@angular/common'; + +@Component({ + selector: 'app-movie', + standalone: true, + imports: [DatePipe, RouterModule], + templateUrl: './movie.component.html', + styleUrl: './movie.component.css' +}) +export class MovieComponent { + public imagesBaseUrl = imagesBaseUrl; + @Input() movie!: Movie; +} diff --git a/src/app/components/movies-scroller/movies-scroller.component.css b/src/app/components/movies-scroller/movies-scroller.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/movies-scroller/movies-scroller.component.html b/src/app/components/movies-scroller/movies-scroller.component.html new file mode 100644 index 0000000..1058d3f --- /dev/null +++ b/src/app/components/movies-scroller/movies-scroller.component.html @@ -0,0 +1,14 @@ +
+
+ @if(dataObs | async; as popularMovies){ + @for(movie of popularMovies; track movie.id){ + + } + } +
+ + +
\ No newline at end of file diff --git a/src/app/components/movies-scroller/movies-scroller.component.spec.ts b/src/app/components/movies-scroller/movies-scroller.component.spec.ts new file mode 100644 index 0000000..813154c --- /dev/null +++ b/src/app/components/movies-scroller/movies-scroller.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MoviesScrollerComponent } from './movies-scroller.component'; + +describe('MoviesScrollerComponent', () => { + let component: MoviesScrollerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MoviesScrollerComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MoviesScrollerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/movies-scroller/movies-scroller.component.ts b/src/app/components/movies-scroller/movies-scroller.component.ts new file mode 100644 index 0000000..604426c --- /dev/null +++ b/src/app/components/movies-scroller/movies-scroller.component.ts @@ -0,0 +1,26 @@ +import { AsyncPipe } from '@angular/common'; +import { Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { MovieComponent } from '../movie/movie.component'; +import { Observable } from 'rxjs'; +import { Movie } from '../../models/movie'; + +@Component({ + selector: 'app-movies-scroller', + standalone: true, + imports: [AsyncPipe, MovieComponent], + templateUrl: './movies-scroller.component.html', + styleUrl: './movies-scroller.component.css' +}) +export class MoviesScrollerComponent { + @Input() dataObs!: Observable; + @ViewChild('content', { read: ElementRef }) + public content!: ElementRef; + + public scrollRight(): void { + this.content.nativeElement.scrollTo({ left: (this.content.nativeElement.scrollLeft + 150), behavior: 'smooth' }); + } + + public scrollLeft(): void { + this.content.nativeElement.scrollTo({ left: (this.content.nativeElement.scrollLeft - 150), behavior: 'smooth' }); + } +} diff --git a/src/app/components/navbar/navbar.component.css b/src/app/components/navbar/navbar.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/navbar/navbar.component.html b/src/app/components/navbar/navbar.component.html new file mode 100644 index 0000000..249c5e8 --- /dev/null +++ b/src/app/components/navbar/navbar.component.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/src/app/components/navbar/navbar.component.spec.ts b/src/app/components/navbar/navbar.component.spec.ts new file mode 100644 index 0000000..5e58f4d --- /dev/null +++ b/src/app/components/navbar/navbar.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavbarComponent } from './navbar.component'; + +describe('NavbarComponent', () => { + let component: NavbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NavbarComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts new file mode 100644 index 0000000..3f1111f --- /dev/null +++ b/src/app/components/navbar/navbar.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +@Component({ + selector: 'app-navbar', + standalone: true, + imports: [RouterModule], + templateUrl: './navbar.component.html', + styleUrl: './navbar.component.css' +}) +export class NavbarComponent { + +} diff --git a/src/app/components/slider/slider.component.css b/src/app/components/slider/slider.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/slider/slider.component.html b/src/app/components/slider/slider.component.html new file mode 100644 index 0000000..b553e28 --- /dev/null +++ b/src/app/components/slider/slider.component.html @@ -0,0 +1,41 @@ +
+ @if(dataObs | async; as movies){ + @for(movie of movies; track movie.id){ + @if($index === slideIndex){ +
+ +
+

+ {{ movie.title }} +

+

{{ movie.overview }}

+

+ Rating: {{ movie.vote_average }} | Popularity: + {{ movie.popularity }} +

+

+ Release Date: + {{ movie.release_date | date }} +

+
+
+ + +
+
+ } + } + } +
\ No newline at end of file diff --git a/src/app/components/slider/slider.component.spec.ts b/src/app/components/slider/slider.component.spec.ts new file mode 100644 index 0000000..62113f0 --- /dev/null +++ b/src/app/components/slider/slider.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SliderComponent } from './slider.component'; + +describe('SliderComponent', () => { + let component: SliderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SliderComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SliderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/slider/slider.component.ts b/src/app/components/slider/slider.component.ts new file mode 100644 index 0000000..d77a460 --- /dev/null +++ b/src/app/components/slider/slider.component.ts @@ -0,0 +1,42 @@ +import { Component, Input } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Movie } from '../../models/movie'; +import { AsyncPipe, DatePipe } from '@angular/common'; + +@Component({ + selector: 'app-slider', + standalone: true, + imports: [AsyncPipe, DatePipe], + templateUrl: './slider.component.html', + styleUrl: './slider.component.css' +}) +export class SliderComponent { + @Input() dataObs!: Observable; + @Input() imagesBaseUrl = ''; + public slideIndex = 0; + + ngOnInit() { + setInterval(() => { + if(this.slideIndex < 19){ + this.slideIndex++; + } + else { + this.slideIndex = 0; + } + }, 5000); + } + + slideLeft() { + if(this.slideIndex <= 0 ){ + return + } + this.slideIndex--; + } + + slideRight(){ + if(this.slideIndex >= 19 ){ + return + } + this.slideIndex++; + } +} diff --git a/src/app/components/video/video.component.css b/src/app/components/video/video.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/video/video.component.html b/src/app/components/video/video.component.html new file mode 100644 index 0000000..80d8b5a --- /dev/null +++ b/src/app/components/video/video.component.html @@ -0,0 +1,12 @@ +@if(key){ + + + +} \ No newline at end of file diff --git a/src/app/components/video/video.component.spec.ts b/src/app/components/video/video.component.spec.ts new file mode 100644 index 0000000..995d09b --- /dev/null +++ b/src/app/components/video/video.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VideoComponent } from './video.component'; + +describe('VideoComponent', () => { + let component: VideoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VideoComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(VideoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/video/video.component.ts b/src/app/components/video/video.component.ts new file mode 100644 index 0000000..d39f663 --- /dev/null +++ b/src/app/components/video/video.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; + +@Component({ + selector: 'app-video', + standalone: true, + imports: [], + templateUrl: './video.component.html', + styleUrl: './video.component.css' +}) +export class VideoComponent { + @Input() key: string | null = null; + videoUrl: SafeResourceUrl = ''; + constructor(private sanitizer: DomSanitizer) {} + + ngOnInit() { + this.videoUrl = this.sanitizer.bypassSecurityTrustResourceUrl( + 'https://www.youtube.com/embed/' + this.key + ) + } +} diff --git a/src/app/models/credit.ts b/src/app/models/credit.ts new file mode 100644 index 0000000..31f7d39 --- /dev/null +++ b/src/app/models/credit.ts @@ -0,0 +1,10 @@ +export interface Credits { + cast: Actor[]; +} + +export interface Actor { + name: string; + profile_path: string; + character: string; + id: number; +} \ No newline at end of file diff --git a/src/app/models/image.ts b/src/app/models/image.ts new file mode 100644 index 0000000..3754a4d --- /dev/null +++ b/src/app/models/image.ts @@ -0,0 +1,7 @@ +export interface Images { + backdrops: Image[]; +} + +export interface Image { + file_path: string; +} \ No newline at end of file diff --git a/src/app/models/movie.ts b/src/app/models/movie.ts new file mode 100644 index 0000000..2285d7c --- /dev/null +++ b/src/app/models/movie.ts @@ -0,0 +1,27 @@ +export interface Movie { + id: number; + adult: boolean; + backdrop_path: string; + genre_ids: number[]; + original_language: string; + original_title: string; + overview: string; + popularity: number; + poster_path: string; + release_date: string; + title: string; + video: boolean; + vote_average: number; + vote_count: number; + revenue?: number; + runtime?: string; + status?: string; + genres?: any[]; +} + +export interface Movies { + page: number; + results: Movie[]; + total_pages: number; + total_results: number; +} \ No newline at end of file diff --git a/src/app/models/video.ts b/src/app/models/video.ts new file mode 100644 index 0000000..0cb84eb --- /dev/null +++ b/src/app/models/video.ts @@ -0,0 +1,9 @@ +export interface Videos { + results: Video[]; + id: string; +} + +export interface Video { + key: string; + site: string; +} \ No newline at end of file diff --git a/src/app/pages/home/home.component.css b/src/app/pages/home/home.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html new file mode 100644 index 0000000..cc47cde --- /dev/null +++ b/src/app/pages/home/home.component.html @@ -0,0 +1,13 @@ +
+ +
+

Popular

+ + +

Top

+ + +

Now Playing

+ +
+
diff --git a/src/app/pages/home/home.component.spec.ts b/src/app/pages/home/home.component.spec.ts new file mode 100644 index 0000000..f94d44e --- /dev/null +++ b/src/app/pages/home/home.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HomeComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts new file mode 100644 index 0000000..a1eff34 --- /dev/null +++ b/src/app/pages/home/home.component.ts @@ -0,0 +1,26 @@ +import { Component, inject } from '@angular/core'; +import { imagesBaseUrl, MoviesService } from '../../services/movies.service'; +import { map } from 'rxjs'; +import { SliderComponent } from '../../components/slider/slider.component'; +import { MoviesScrollerComponent } from '../../components/movies-scroller/movies-scroller.component'; + +@Component({ + selector: 'app-home', + standalone: true, + imports: [SliderComponent, MoviesScrollerComponent], + templateUrl: './home.component.html', + styleUrl: './home.component.css' +}) +export class HomeComponent { + private moviesService = inject(MoviesService); + public imagesBaseUrl = imagesBaseUrl; + public popularMovies$ = this.moviesService + .fetchMoviesByType('popular') + .pipe(map((data) => data.results)); + public topRatedMovies$ = this.moviesService + .fetchMoviesByType('top_rated') + .pipe(map((data) => data.results)); + public nowPlayingMovies$ = this.moviesService + .fetchMoviesByType('now_playing') + .pipe(map((data) => data.results)); +} diff --git a/src/app/pages/movies/movies.component.css b/src/app/pages/movies/movies.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/movies/movies.component.html b/src/app/pages/movies/movies.component.html new file mode 100644 index 0000000..91a0e1a --- /dev/null +++ b/src/app/pages/movies/movies.component.html @@ -0,0 +1,7 @@ +
+
+ @for(movie of moviesResults; track movie.id){ + + } +
+
\ No newline at end of file diff --git a/src/app/pages/movies/movies.component.spec.ts b/src/app/pages/movies/movies.component.spec.ts new file mode 100644 index 0000000..6100dd5 --- /dev/null +++ b/src/app/pages/movies/movies.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MoviesComponent } from './movies.component'; + +describe('MoviesComponent', () => { + let component: MoviesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MoviesComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MoviesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/movies/movies.component.ts b/src/app/pages/movies/movies.component.ts new file mode 100644 index 0000000..1b85065 --- /dev/null +++ b/src/app/pages/movies/movies.component.ts @@ -0,0 +1,36 @@ +import { Component, DestroyRef, inject } from '@angular/core'; +import { MovieComponent } from '../../components/movie/movie.component'; +import { InfiniteScrollModule } from "ngx-infinite-scroll"; +import { MoviesService } from '../../services/movies.service'; +import { Movie } from '../../models/movie'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AsyncPipe } from '@angular/common'; + +@Component({ + selector: 'app-movies', + standalone: true, + imports: [AsyncPipe, MovieComponent, InfiniteScrollModule], + templateUrl: './movies.component.html', + styleUrl: './movies.component.css' +}) +export class MoviesComponent { + private moviesService = inject(MoviesService); + private pageNumber = 1; + private destroyRef = inject(DestroyRef) + public moviesObs$ = this.moviesService.fetchMoviesByType('popular', this.pageNumber); + public moviesResults: Movie[] = []; + + ngOnInit() { + this.moviesObs$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => { + this.moviesResults = data.results; + }) + } + + onScroll() { + this.pageNumber++; + this.moviesObs$ = this.moviesService.fetchMoviesByType('popular', this.pageNumber); + this.moviesObs$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => { + this.moviesResults = this.moviesResults.concat(data.results); + }) + } +} diff --git a/src/app/pages/show-movie/show-movie.component.css b/src/app/pages/show-movie/show-movie.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/show-movie/show-movie.component.html b/src/app/pages/show-movie/show-movie.component.html new file mode 100644 index 0000000..b8078dd --- /dev/null +++ b/src/app/pages/show-movie/show-movie.component.html @@ -0,0 +1,72 @@ +
+

Hello

+ @if(movieObs$ | async; as movie){ + @if(movie.poster_path){ +
+
+ +
+
+
+ + +
+
+

+ {{ movie.title }} +

+

{{ movie.overview }}

+

+ Rating: {{ movie.vote_average }} | Popularity: + {{ movie.popularity }} | Duration : {{ movie.runtime }} +

+

+ Status: {{ movie.status }} | Release Date: + {{ movie.release_date | date }} | Revenue: {{ movie.revenue | currency }} +

+

Cast:

+
+ @if(movieCastObs$ | async; as movieCast) { + @for(actor of movieCast; track actor.id){ +
+ +

{{ actor.name }}

+
+ } + } +
+
+
+
+

Similar Movies

+ +
+
+ } + } + + @if(showVideo){ + @if(movieVideosObs$ | async; as videos){ +
+
+ + +
+
+ } + } +
diff --git a/src/app/pages/show-movie/show-movie.component.spec.ts b/src/app/pages/show-movie/show-movie.component.spec.ts new file mode 100644 index 0000000..b7831d8 --- /dev/null +++ b/src/app/pages/show-movie/show-movie.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ShowMovieComponent } from './show-movie.component'; + +describe('ShowMovieComponent', () => { + let component: ShowMovieComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ShowMovieComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ShowMovieComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/show-movie/show-movie.component.ts b/src/app/pages/show-movie/show-movie.component.ts new file mode 100644 index 0000000..30e3a55 --- /dev/null +++ b/src/app/pages/show-movie/show-movie.component.ts @@ -0,0 +1,45 @@ +import { AsyncPipe, CurrencyPipe, DatePipe } from '@angular/common'; +import { Component, inject, Input } from '@angular/core'; +import { MoviesScrollerComponent } from '../../components/movies-scroller/movies-scroller.component'; +import { imagesBaseUrl, MoviesService } from '../../services/movies.service'; +import { map, Observable } from 'rxjs'; +import { Movie } from '../../models/movie'; +import { Actor } from '../../models/credit'; +import { Video } from '../../models/video'; +import { ActivatedRoute } from '@angular/router'; +import { VideoComponent } from '../../components/video/video.component'; + +@Component({ + selector: 'app-show-movie', + standalone: true, + imports: [AsyncPipe, DatePipe, CurrencyPipe, MoviesScrollerComponent, VideoComponent], + templateUrl: './show-movie.component.html', + styleUrl: './show-movie.component.css' +}) +export class ShowMovieComponent { + @Input() movieId: string = ''; + private moviesService = inject(MoviesService); + public movieObs$! : Observable; + public movieCastObs$!: Observable; + public movieVideosObs$! : Observable; + public similarMoviesObs$!: Observable; + public imagesBaseUrl = imagesBaseUrl; + private activatedRouter = inject(ActivatedRoute); + public showVideo = false; + + ngOnInit() { + this.activatedRouter.params.pipe(map((p) => p['movieId'])).subscribe((id)=> { + this.movieObs$ = this.moviesService.fetchMovieById(id); + this.movieCastObs$ = this.moviesService.fetchMovieCast(id); + this.movieVideosObs$ = this.moviesService.fetchMovieVideos(id); + this.similarMoviesObs$ = this.moviesService.fetchSimilarMovies(id); + }) + } + + openVideo(){ + this.showVideo = true; + } + closeVideo(){ + this.showVideo = false; + } +} diff --git a/src/app/services/movies.service.spec.ts b/src/app/services/movies.service.spec.ts new file mode 100644 index 0000000..364e63b --- /dev/null +++ b/src/app/services/movies.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MoviesService } from './movies.service'; + +describe('MoviesService', () => { + let service: MoviesService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MoviesService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/movies.service.ts b/src/app/services/movies.service.ts new file mode 100644 index 0000000..4d10100 --- /dev/null +++ b/src/app/services/movies.service.ts @@ -0,0 +1,54 @@ +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { Movie, Movies } from '../models/movie'; +import { map } from 'rxjs'; +import { Videos } from '../models/video'; +import { Credits } from '../models/credit'; + +export const imagesBaseUrl = 'https://image.tmdb.org/t/p/'; + +@Injectable({ + providedIn: 'root' +}) +export class MoviesService { + private apiUrl = 'https://api.themoviedb.org/3'; + private apiKey = environment.apiKEY; + private httpClient = inject(HttpClient); + + constructor() { } + + fetchMoviesByType(type: string, pageNumber = 1) { + return this.httpClient.get(`${this.apiUrl}/movie/${type}?api_key=${this.apiKey}&page=${pageNumber}`); + } + + fetchSimilarMovies(id: string) { + return this.httpClient + .get( + `${this.apiUrl}/movie/${id}/similar?api_key=${this.apiKey}` + ) + .pipe(map((data)=> data.results)); + } + + fetchMovieById(id: string) { + return this.httpClient.get( + `${this.apiUrl}/movie/${id}?api_key=${this.apiKey}` + ) + } + + fetchMovieVideos(id: string) { + return this.httpClient + .get( + `${this.apiUrl}/movie/${id}/videos?api_key=${this.apiKey}` + ) + .pipe(map((data) => data.results)) + } + + fetchMovieCast(id: string) { + return this.httpClient + .get( + `${this.apiUrl}/movie/${id}/credits?api_key=${this.apiKey}` + ) + .pipe(map((data) => data.cast)) + } +} diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts new file mode 100644 index 0000000..fe568f9 --- /dev/null +++ b/src/environments/environment.development.ts @@ -0,0 +1,3 @@ +export const environment = { + apiKEY: 'dadb019730c0075868955d1ec94040bb' +}; \ No newline at end of file diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..fe568f9 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,3 @@ +export const environment = { + apiKEY: 'dadb019730c0075868955d1ec94040bb' +}; \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 9907dc1..796882a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,3 @@ -/* You can add global styles to this file, and also import other style files */ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..0f0c063 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from 'tailwindcss' + +export default { + content: ["./src/**/*.{html,ts}"], + theme: { + extend: {}, + }, + plugins: [], +} satisfies Config +