diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/.example.env b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/.example.env new file mode 100644 index 0000000..4be3474 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/.example.env @@ -0,0 +1,2 @@ +OPENWEATHER_API_KEY=your_openweather_api_key +# you can use this temporary key for testing: 1bce65f6db162dc833ca613dacb014d6 diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/.gitignore b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/.gitignore new file mode 100644 index 0000000..5ac8ea5 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/.gitignore @@ -0,0 +1,9 @@ +# Ignore environment variables +.env + +# Ignore dist folder +dist +js + +# Ignore node_modules +node_modules diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/README.md b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/README.md new file mode 100644 index 0000000..ea434fa --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/README.md @@ -0,0 +1,121 @@ +# Weather CLI + +A Node.js command-line app that fetches real-time weather data from [OpenWeatherMap](https://openweathermap.org/), built with TypeScript. + +Supports current conditions, hourly forecasts, and multi-day forecasts — with both inline and interactive REPL modes. + +--- + +## Features + +- Current weather, hourly forecast, and daily forecast +- Inline mode for quick one-shot queries +- Interactive REPL mode for continuous querying +- In-memory cache with TTL to avoid redundant API calls +- Auto-detects your city via IP if no location is provided + +--- + +## Setup + +### 1. Install dependencies + +```bash +npm install +``` + +### 2. Configure environment variables + +Create a `.env` file in the project root: + +```env +OPENWEATHER_API_KEY=your_openweather_api_key +``` + +- Get an OpenWeatherMap API key at [openweathermap.org](https://home.openweathermap.org/users/sign_up) + +--- + +## Usage + +### Inline mode + +```bash +npm start -- [location] [options] +``` + +| Option | Description | +| ------------------ | --------------------------------------- | +| `-H, --hourly [n]` | Hourly forecast (`n` hours, default: 4) | +| `-d, --daily [n]` | Daily forecast (`n` days, default: 7) | +| `--no-cache` | Disable cache for this request | +| `--show-cache` | Print cache state after fetching | + +**Examples:** + +```bash +# Current weather in Algiers +npm start -- algiers + +# 8-hour forecast +npm start -- blida -H 8 + +# 5-day forecast with no cache +npm start -- "new york" -d 5 --no-cache + +# Auto-detect city +npm start +``` + +### Interactive mode + +```bash +npm start -- -i +# or +npm start -- --interactive +``` + +Inside the REPL: + +``` +weather-cli> weather algiers +weather-cli> weather algiers -H 6 +weather-cli> weather algiers -d 3 +weather-cli> help +weather-cli> clear +weather-cli> exit +``` + +--- + +## Project Structure + +``` +src/ +├── index.ts # Entry point +├── interfaces.ts # Core interfaces +├── types.ts # DTOs and shared types +├── core/ +│ ├── cli.ts # CLI orchestrator (inline + interactive) +│ ├── weather.ts # WeatherService (caching, forecasting) +│ └── weather-command.ts # Shared Commander command builder +├── providers/ +│ ├── openweather.ts # OpenWeatherMap API client +│ └── cache/ +│ ├── local-cache.ts # In-memory cache with TTL +│ └── file-cache.ts # File-based cache (not yet implemented) +└── utils/ + ├── helpers.ts # Includes city auto-detection + ├── mappers.ts # API response → DTO transformers + └── printers.ts # Formatted console output +``` + +--- + +## Scripts + +| Script | Description | +| ----------------- | -------------------------------- | +| `npm start` | Run with `tsx` (no build needed) | +| `npm run build` | Compile TypeScript to `dist/` | +| `npm run preview` | Run compiled output from `dist/` | diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/package-lock.json b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/package-lock.json new file mode 100644 index 0000000..c9558f1 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/package-lock.json @@ -0,0 +1,629 @@ +{ + "name": "submission", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "submission", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chalk": "^5.6.2", + "commander": "^14.0.3", + "dotenv": "^17.3.1" + }, + "devDependencies": { + "@types/node": "^25.3.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/package.json b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/package.json new file mode 100644 index 0000000..a806487 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/package.json @@ -0,0 +1,26 @@ +{ + "name": "submission", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "start": "tsx src/index.ts", + "build": "tsc", + "preview": "node dist/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "devDependencies": { + "@types/node": "^25.3.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + }, + "dependencies": { + "chalk": "^5.6.2", + "commander": "^14.0.3", + "dotenv": "^17.3.1" + } +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/core/cli.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/core/cli.ts new file mode 100644 index 0000000..f44ef54 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/core/cli.ts @@ -0,0 +1,118 @@ +import type { + ICacheService, + ICliService, + IWeatherService, +} from '../interfaces.js'; +import { buildWeatherCommand } from './weather-command.js'; +import repl from 'node:repl'; + +export class CliService implements ICliService { + constructor( + private weatherClient: IWeatherService, + private cacheProvider: ICacheService, + ) {} + private _inline: boolean = false; + + inline() { + this._inline = true; + return this; + } + + async run(): Promise { + if (this._inline) { + await this.runInline(); + } else { + await this.runInteractive(); + } + } + + private async runInline(args?: string[]): Promise { + const program = buildWeatherCommand(this.weatherClient, this.cacheProvider); + program + .name('weather') + .description('Weather CLI tool') + .version('1.0.0') + .exitOverride() // prevent commander from exiting the process on error + .configureOutput({ + writeErr: () => {}, + }); + if (args) { + await program.parseAsync(args, { from: 'user' }); + } else { + await program.parseAsync(); + } + } + + private async runInteractive(): Promise { + return new Promise((resolve) => { + console.log('--- Interactive Weather Mode ---'); + console.log('Type "weather [options]", "help", or "exit".\n'); + + const replServer = repl.start({ + prompt: 'weather-cli> ', + ignoreUndefined: true, + eval: async (cmd, _context, _filename, callback) => { + const input = cmd.trim(); + const [command, ...rest] = input.split(' '); + + switch (command) { + case '': + break; + + case 'exit': + replServer.close(); + break; + + case 'help': + console.log('Usage: weather [options]'); + console.log('Options:'); + console.log( + ' -H, --hourly [n] Show hourly forecast (default: 4 hours)', + ); + console.log( + ' -d, --daily [n] Show daily forecast (default: 7 days)', + ); + console.log(' --no-cache Disable cache'); + console.log( + ' --show-cache Show cache state after fetching data', + ); + console.log('\nCommands: weather, help, exit\n'); + break; + + case 'weather': { + try { + const args = rest.flatMap( + (a) => a.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [], + ); + await this.runInline(args); + } catch (error) { + const message = + error instanceof Error ? error.message : String(error); + console.error(`❌ ${message}`); + } + break; + } + + case 'clear': { + // clear terminal + process.stdout.write('\x1Bc'); + break; + } + + default: + console.error( + `Unknown command: "${command}". Type "help" for usage.`, + ); + } + + callback(null, undefined); + }, + }); + + replServer.on('exit', () => { + console.log('\nGoodbye!'); + resolve(); + }); + }); + } +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/core/weather-command.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/core/weather-command.ts new file mode 100644 index 0000000..7fb0a99 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/core/weather-command.ts @@ -0,0 +1,67 @@ +import { Command } from 'commander'; +import { + printCurrentWeather, + printDailyForecast, + printHourlyForecast, +} from '../utils/printers.js'; +import type { + CurrentWeatherDto, + DailyForecastDto, + HourlyForecastDto, +} from '../types.js'; +import type { ICacheService, IWeatherService } from '../interfaces.js'; + +export function buildWeatherCommand( + weatherClient: IWeatherService, + cacheProvider: ICacheService, +): Command { + const program = new Command(); + + program + .argument('[location]', 'City name or zip code') + .option('-H, --hourly [hours]', 'Show hourly forecast') + .option('-d, --daily [days]', 'Show daily forecast') + .option('--no-cache', 'Disable cache') + .option('--show-cache', 'Show cache state after fetching data') + .action(async (location, options) => { + const { hourly, daily, cache, showCache } = options; + const forecastFlags = ['hourly', 'daily'].filter((key) => options[key]); + if (forecastFlags.length > 1) { + console.error( + `❌ Only one forecast type is allowed at a time. You passed: ${forecastFlags.join(', ')}`, + ); + return; + } + + let forecastType: 'current' | 'hourly' | 'daily' = 'current'; + if (hourly) forecastType = 'hourly'; + if (daily) forecastType = 'daily'; + + let count: number | undefined = + parseInt(hourly, 10) || parseInt(daily, 10); + count = isNaN(count) ? undefined : count; + + console.log('🔍 Fetching weather data...'); + + weatherClient.reset().forecast(forecastType); + if (!cache) weatherClient.noCache(); + + const data = await weatherClient.getWeather(location, count); + switch (forecastType) { + case 'current': + printCurrentWeather(data as CurrentWeatherDto); + break; + case 'daily': + printDailyForecast(data as DailyForecastDto, count); + break; + case 'hourly': + printHourlyForecast(data as HourlyForecastDto, count); + break; + } + + if (showCache) { + cacheProvider.dump(); + } + }); + return program; +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/core/weather.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/core/weather.ts new file mode 100644 index 0000000..471b539 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/core/weather.ts @@ -0,0 +1,57 @@ +import 'dotenv/config'; +import type { + ICacheService, + IWeatherProvider, + IWeatherService, +} from '../interfaces.js'; +import type { WeatherDto } from '../types.js'; +import { detectCity } from '../utils/helpers.js'; + +export class WeatherService implements IWeatherService { + private _forecast: 'current' | 'hourly' | 'daily' = 'current'; + private _noCache: boolean = false; + + constructor( + protected readonly weatherProvider: IWeatherProvider, + protected readonly cacheProvider: ICacheService, + ) {} + + reset() { + this._forecast = 'current'; + this._noCache = false; + return this; + } + + forecast(forecast: 'current' | 'hourly' | 'daily') { + this._forecast = forecast; + return this; + } + + noCache() { + this._noCache = true; + return this; + } + + async getWeather(location: string, count?: number) { + if (this._noCache === false) { + const cached = await this.cacheProvider.get( + `${this._forecast}:${location}`, + ); + if (cached) return cached.value as WeatherDto; + } + + this.weatherProvider.forecast(this._forecast); + + if (!location) { + location = await detectCity(); + } + + const data = await this.weatherProvider.getWeather(location, count); + + if (this._noCache === false) { + await this.cacheProvider.set(`${this._forecast}:${location}`, data); + } + + return data; + } +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/index.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/index.ts new file mode 100644 index 0000000..a0f3a77 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/index.ts @@ -0,0 +1,48 @@ +import { CliService } from './core/cli.js'; +import { WeatherService } from './core/weather.js'; +import type { ICacheService } from './interfaces.js'; +import { LocalCacheProvider } from './providers/cache/local-cache.js'; +import { OpenWeatherApi } from './providers/openweather.js'; + +const args = process.argv.slice(2); + +let cacheProvider: ICacheService; + +function initCli() { + const isInteractive = args.includes('-i') || args.includes('--interactive'); + + if (isInteractive) { + cacheProvider = new LocalCacheProvider(); + } else { + // cacheProvider = new FileCacheProvider(); not implemented yet + cacheProvider = new LocalCacheProvider(); + } + + const weatherProvider = new OpenWeatherApi(process.env.OPENWEATHER_API_KEY!); + const weatherClient = new WeatherService(weatherProvider, cacheProvider); + + const cliClient = new CliService(weatherClient, cacheProvider); + + if (!isInteractive) { + return cliClient.inline(); + } + + return cliClient; +} + +async function start() { + try { + const cliClient = initCli(); + await cliClient.run(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('Error:', message); + process.exitCode = 1; + } finally { + if (cacheProvider.clearTimers) { + cacheProvider.clearTimers(); // Clear any pending cache expiration timers + } + } +} + +start(); diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/interfaces.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/interfaces.ts new file mode 100644 index 0000000..8819d90 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/interfaces.ts @@ -0,0 +1,27 @@ +import type { CacheEntry, CacheStats, WeatherDto } from './types.js'; + +export interface IWeatherProvider { + forecast(forecast: 'current' | 'hourly' | 'daily'): IWeatherProvider; + getWeather(city: string, count?: number): Promise; + reset(): IWeatherProvider; +} + +export interface IWeatherService extends IWeatherProvider { + noCache(): IWeatherService; +} +export interface ICacheService { + defaultTimeout?: number; + has(key: string): Promise; + get(key: string): Promise; + set(key: string, value: any, timeout?: number): Promise; + delete(key: string): Promise; + clear(): Promise; + getStats(): CacheStats; + dump(): void; + clearTimers?(): void; +} + +export interface ICliService { + run(): Promise; + inline(): ICliService; +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/providers/cache/file-cache.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/providers/cache/file-cache.ts new file mode 100644 index 0000000..bf24a36 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/providers/cache/file-cache.ts @@ -0,0 +1,33 @@ +import type { ICacheService } from '../../interfaces.js'; +import type { CacheStats } from '../../types.js'; + +export class FileCacheProvider implements ICacheService { + // to be implemented: read/write from filesystem + async has(key: string): Promise { + throw new Error('FileCacheProvider not implemented'); + } + + async get(key: string): Promise { + throw new Error('FileCacheProvider not implemented'); + } + + async set(key: string, value: T): Promise { + throw new Error('FileCacheProvider not implemented'); + } + + async delete(key: string): Promise { + throw new Error('FileCacheProvider not implemented'); + } + + async clear(): Promise { + throw new Error('FileCacheProvider not implemented'); + } + + getStats(): CacheStats { + throw new Error('FileCacheProvider not implemented'); + } + + dump() { + throw new Error('FileCacheProvider not implemented'); + } +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/providers/cache/local-cache.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/providers/cache/local-cache.ts new file mode 100644 index 0000000..bde46bc --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/providers/cache/local-cache.ts @@ -0,0 +1,117 @@ +import type { ICacheService } from '../../interfaces.js'; +import type { CacheEntry, CacheStats } from '../../types.js'; + +export class LocalCacheProvider implements ICacheService { + private store = new Map(); + private timers = new Map(); + private stats: CacheStats = { + hits: 0, + misses: 0, + keys: 0, + }; + + constructor(public defaultTimeout: number = 5 * 60 * 1000) {} // default 5 minutes + + async has(key: string): Promise { + return this.store.has(key); + } + + async get(key: string): Promise { + const entry = this.store.get(key); + + if (!entry) { + this.stats.misses++; + return null; + } + + // Check expiration + if (entry.expiresAt && Date.now() > entry.expiresAt) { + await this.delete(key); + this.stats.misses++; + return null; + } + + this.stats.hits++; + return entry; + } + + async set( + key: string, + value: any, + timeout = this.defaultTimeout, + ): Promise { + // Clear existing timer if any + if (this.timers.has(key)) { + clearTimeout(this.timers.get(key)!); + } + + const entry: CacheEntry = { + value, + expiresAt: Date.now() + timeout, + }; + + this.store.set(key, entry); + this.stats.keys = this.store.size; + + if (timeout) { + const timer = setTimeout(() => { + this.delete(key); + }, timeout); + + this.timers.set(key, timer); + } + } + + async delete(key: string): Promise { + this.store.delete(key); + + if (this.timers.has(key)) { + clearTimeout(this.timers.get(key)!); + this.timers.delete(key); + } + + this.stats.keys = this.store.size; + } + + async clear(): Promise { + this.store.clear(); + + for (const timer of this.timers.values()) { + clearTimeout(timer); + } + + this.timers.clear(); + this.stats.keys = 0; + } + + // Returns structured stats + getStats(): CacheStats { + return { ...this.stats }; + } + + // Prints full cache state + dump(): void { + console.log('------------------------'); + console.log('Cache State:'); + console.log('------------------------'); + + for (const [key, entry] of this.store.entries()) { + console.log({ + key, + expiresAt: entry.expiresAt + ? new Date(entry.expiresAt).toISOString() + : 'never', + }); + } + + console.log('------------------------'); + console.log('Stats:', this.getStats()); + } + + clearTimers(): void { + for (const timer of this.timers.values()) { + clearTimeout(timer); + } + this.timers.clear(); + } +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/providers/openweather.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/providers/openweather.ts new file mode 100644 index 0000000..bd49ec8 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/providers/openweather.ts @@ -0,0 +1,75 @@ +import type { IWeatherProvider } from '../interfaces.js'; +import { + toCurrentWeatherDto, + toDailyForecastDto, + toHourlyForecastDto, +} from '../utils/mappers.js'; + +export class OpenWeatherApi implements IWeatherProvider { + private apiKey: string; + private baseUrl: string = 'https://api.openweathermap.org/data/2.5'; + + private _forecast: 'current' | 'hourly' | 'daily' = 'current'; + + constructor(apiKey: string) { + if (!apiKey) { + throw new Error('OpenWeather API key is required'); + } + this.apiKey = apiKey; + } + + reset(): IWeatherProvider { + this._forecast = 'current'; + return this; + } + + forecast(forecast: 'current' | 'hourly' | 'daily'): IWeatherProvider { + this._forecast = forecast; + return this; + } + + private buildUrl( + endpoint: string, + params: Record, + ): string { + const query = new URLSearchParams({ + ...params, + appid: this.apiKey, + }).toString(); + return `${this.baseUrl}/${endpoint}?${query}`; + } + + async getWeather(location: string, count?: number) { + let endpoint: string; + let mapper; + switch (this._forecast) { + case 'current': + endpoint = 'weather'; + mapper = toCurrentWeatherDto; + break; + case 'hourly': + endpoint = 'forecast'; + mapper = toHourlyForecastDto; + break; + case 'daily': + endpoint = 'forecast/daily'; + mapper = toDailyForecastDto; + break; + default: + throw new Error('Invalid forecast type'); + } + + const url = this.buildUrl(endpoint, { + q: location, + cnt: count || (this._forecast === 'hourly' ? 4 : 7), + }); + const res = await fetch(url); + const data = (await res.json()) as any; + + if (!res.ok) { + throw new Error(`${data.message || res.statusText}`); + } + + return mapper(data); + } +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/types.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/types.ts new file mode 100644 index 0000000..f703602 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/types.ts @@ -0,0 +1,145 @@ +// ------------------------------ +// Shared Types +// ------------------------------ +export interface CacheStats { + hits: number; + misses: number; + keys: number; +} + +export interface CacheEntry { + expiresAt: number; + value: any; +} + +export interface CoordinatesDto { + lat: number; + lon: number; +} + +export interface WeatherConditionDto { + id: number; + main: string; + description: string; + icon: string; + iconUrl: string; +} + +// ------------------------------ +// Current Weather DTO +// ------------------------------ + +export interface CurrentWeatherDto { + city: string; + country: string; + coordinates: CoordinatesDto; + + temperature: number; // °C + feelsLike: number; // °C + minTemperature: number; // °C + maxTemperature: number; // °C + + humidity: number; // % + pressure: number; // hPa + visibility: number; // meters + + windSpeed: number; // m/s + windDirection: number; // degrees + windGust?: number; + + cloudiness: number; // % + rainLastHour?: number; // mm + + condition: WeatherConditionDto; + + sunrise: Date; + sunset: Date; + timestamp: Date; +} + +// ------------------------------ +// Daily Forecast DTO +// ------------------------------ + +export interface DailyForecastDto { + city: string; + country: string; + timezone: number; + days: DailyForecastItemDto[]; +} + +export interface DailyForecastItemDto { + date: Date; + + temperature: { + day: number; + min: number; + max: number; + night: number; + morning: number; + evening: number; + }; + + feelsLike: { + day: number; + night: number; + morning: number; + evening: number; + }; + + humidity: number; + pressure: number; + + windSpeed: number; + windDirection: number; + windGust?: number; + + cloudiness: number; + precipitationProbability: number; // 0–1 + rain?: number; // mm + + condition: WeatherConditionDto; + + sunrise: Date; + sunset: Date; +} + +// ------------------------------ +// Hourly Forecast DTO +// ------------------------------ + +export interface HourlyForecastDto { + city: string; + country: string; + timezone: number; + hours: HourlyForecastItemDto[]; +} + +export interface HourlyForecastItemDto { + date: Date; + + temperature: number; + feelsLike: number; + + humidity: number; + pressure: number; + + windSpeed: number; + windDirection: number; + windGust?: number; + + cloudiness: number; + visibility: number; + + precipitationProbability: number; + rainLastHour?: number; + + condition: WeatherConditionDto; + + partOfDay: 'day' | 'night'; +} + +export type WeatherDto = + | CurrentWeatherDto + | DailyForecastDto + | HourlyForecastDto; diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/utils/helpers.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/utils/helpers.ts new file mode 100644 index 0000000..0476524 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/utils/helpers.ts @@ -0,0 +1,25 @@ +async function getIpAddress() { + const res = await fetch('https://api.ipify.org'); + if (!res.ok) { + throw new Error(`Failed to get IP address: ${res.statusText}`); + } + return (await res.text()).trim(); +} + +export async function detectCity() { + const ip = await getIpAddress(); + const res = await fetch(`http://ip-api.com/json/${ip}`); + const data = (await res.json()) as any; + + if (!res.ok) { + throw new Error( + `Failed to detect location: ${data.error?.message || res.statusText}`, + ); + } + + if (!data || !data.city) { + throw new Error('Unable to detect location'); + } + + return data.city; +} diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/utils/mappers.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/utils/mappers.ts new file mode 100644 index 0000000..a9d2e83 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/utils/mappers.ts @@ -0,0 +1,124 @@ +import type { + CurrentWeatherDto, + DailyForecastDto, + DailyForecastItemDto, + HourlyForecastDto, + HourlyForecastItemDto, + WeatherConditionDto, + CoordinatesDto, +} from '../types.js'; + +// ------------------------------ +// Helpers +// ------------------------------ + +const kelvinToCelsius = (k: number) => +(k - 273.15).toFixed(1); + +const mapWeatherCondition = (w: { + id: number; + main: string; + description: string; + icon: string; +}): WeatherConditionDto => ({ + ...w, + iconUrl: `https://openweathermap.org/img/wn/${w.icon}@2x.png`, +}); + +const mapCoordinates = (lat: number, lon: number): CoordinatesDto => ({ + lat, + lon, +}); + +// ------------------------------ +// Current Weather Mapper +// ------------------------------ + +export const toCurrentWeatherDto = (data: any): CurrentWeatherDto => ({ + city: data.name, + country: data.sys.country, + coordinates: mapCoordinates(data.coord.lat, data.coord.lon), + temperature: kelvinToCelsius(data.main.temp), + feelsLike: kelvinToCelsius(data.main.feels_like), + minTemperature: kelvinToCelsius(data.main.temp_min), + maxTemperature: kelvinToCelsius(data.main.temp_max), + humidity: data.main.humidity, + pressure: data.main.pressure, + visibility: data.visibility, + windSpeed: data.wind.speed, + windDirection: data.wind.deg, + windGust: data.wind.gust, + cloudiness: data.clouds.all, + rainLastHour: data.rain?.['1h'], + condition: mapWeatherCondition(data.weather[0]), + sunrise: new Date(data.sys.sunrise * 1000), + sunset: new Date(data.sys.sunset * 1000), + timestamp: new Date(data.dt * 1000), +}); + +// ------------------------------ +// Daily Forecast Mapper +// ------------------------------ + +export const toDailyForecastDto = (data: any): DailyForecastDto => ({ + city: data.city.name, + country: data.city.country, + timezone: data.city.timezone, + days: data.list.map( + (day: any): DailyForecastItemDto => ({ + date: new Date(day.dt * 1000), + temperature: { + day: kelvinToCelsius(day.temp.day), + min: kelvinToCelsius(day.temp.min), + max: kelvinToCelsius(day.temp.max), + night: kelvinToCelsius(day.temp.night), + morning: kelvinToCelsius(day.temp.morn), + evening: kelvinToCelsius(day.temp.eve), + }, + feelsLike: { + day: kelvinToCelsius(day.feels_like.day), + night: kelvinToCelsius(day.feels_like.night), + morning: kelvinToCelsius(day.feels_like.morn), + evening: kelvinToCelsius(day.feels_like.eve), + }, + humidity: day.humidity, + pressure: day.pressure, + windSpeed: day.speed, + windDirection: day.deg, + windGust: day.gust, + cloudiness: day.clouds, + precipitationProbability: day.pop, + rain: day.rain, + condition: mapWeatherCondition(day.weather[0]), + sunrise: new Date(day.sunrise * 1000), + sunset: new Date(day.sunset * 1000), + }), + ), +}); + +// ------------------------------ +// Hourly Forecast Mapper +// ------------------------------ + +export const toHourlyForecastDto = (data: any): HourlyForecastDto => ({ + city: data.city.name, + country: data.city.country, + timezone: data.city.timezone, + hours: data.list.map( + (hour: any): HourlyForecastItemDto => ({ + date: new Date(hour.dt * 1000), + temperature: kelvinToCelsius(hour.main.temp), + feelsLike: kelvinToCelsius(hour.main.feels_like), + humidity: hour.main.humidity, + pressure: hour.main.pressure, + windSpeed: hour.wind.speed, + windDirection: hour.wind.deg, + windGust: hour.wind.gust, + cloudiness: hour.clouds.all, + visibility: hour.visibility, + precipitationProbability: hour.pop, + rainLastHour: hour.rain?.['1h'], + condition: mapWeatherCondition(hour.weather[0]), + partOfDay: hour.sys.pod === 'd' ? 'day' : 'night', + }), + ), +}); diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/utils/printers.ts b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/utils/printers.ts new file mode 100644 index 0000000..7e81236 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/src/utils/printers.ts @@ -0,0 +1,121 @@ +import chalk from 'chalk'; +import type { + CurrentWeatherDto, + DailyForecastDto, + HourlyForecastDto, +} from '../types.js'; + +const weatherEmoji = (main: string) => { + switch (main.toLowerCase()) { + case 'clear': + return '☀️'; + case 'clouds': + return '☁️'; + case 'rain': + return '🌧️'; + case 'snow': + return '❄️'; + case 'thunderstorm': + return '⛈️'; + case 'drizzle': + return '🌦️'; + case 'mist': + case 'fog': + return '🌫️'; + default: + return ''; + } +}; + +// ------------------------------ +// Current Weather Printer +// ------------------------------ +export const printCurrentWeather = (data: CurrentWeatherDto) => { + console.log(chalk.blue.bold(`\n🌆 Weather in ${data.city}, ${data.country}`)); + console.log( + `${weatherEmoji(data.condition.main)} ${chalk.yellow.bold(data.condition.description)}`, + ); + console.log( + `🌡️ Temperature: ${data.temperature}°C (feels like ${data.feelsLike}°C)`, + ); + console.log( + `🌡️ Min/Max: ${data.minTemperature}°C / ${data.maxTemperature}°C`, + ); + console.log(`💧 Humidity: ${data.humidity}%`); + console.log( + `💨 Wind: ${data.windSpeed} m/s, ${data.windDirection}° ${data.windGust ? `(gusts ${data.windGust} m/s)` : ''}`, + ); + if (data.rainLastHour) + console.log(`🌧️ Rain last hour: ${data.rainLastHour} mm`); + console.log(`☁️ Cloudiness: ${data.cloudiness}%`); + console.log( + `🌅 Sunrise: ${data.sunrise.toLocaleTimeString()} | 🌇 Sunset: ${data.sunset.toLocaleTimeString()}`, + ); + console.log(chalk.gray(`⏰ Updated: ${data.timestamp.toLocaleString()}`)); +}; + +// ------------------------------ +// Daily Forecast Printer +// ------------------------------ +export const printDailyForecast = (data: DailyForecastDto, daysCount = 7) => { + console.log( + chalk.blue.bold( + `\n📅 ${daysCount}-Day Forecast for ${data.city}, ${data.country}\n`, + ), + ); + + data.days.slice(0, daysCount).forEach((day) => { + const dateStr = day.date.toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + }); + console.log(chalk.green.bold(dateStr)); + console.log( + `${weatherEmoji(day.condition.main)} ${chalk.yellow(day.condition.description)}`, + ); + console.log( + `🌡️ Day: ${day.temperature.day}°C | Night: ${day.temperature.night}°C`, + ); + console.log( + `💧 Humidity: ${day.humidity}% | ☁️ Cloudiness: ${day.cloudiness}% | 🌦️ Rain: ${day.rain ?? 0} mm`, + ); + console.log(`💨 Wind: ${day.windSpeed} m/s, ${day.windDirection}°`); + console.log( + `🌅 Sunrise: ${day.sunrise.toLocaleTimeString()} | 🌇 Sunset: ${day.sunset.toLocaleTimeString()}\n`, + ); + }); +}; + +// ------------------------------ +// Hourly Forecast Printer +// ------------------------------ +export const printHourlyForecast = ( + data: HourlyForecastDto, + hoursCount = 4, +) => { + console.log( + chalk.blue.bold( + `\n⏱️ Hourly Forecast for ${data.city}, ${data.country} (next ${hoursCount} hours)\n`, + ), + ); + + data.hours.slice(0, hoursCount).forEach((hour) => { + const timeStr = hour.date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + console.log(chalk.green.bold(timeStr)); + console.log( + `${weatherEmoji(hour.condition.main)} ${chalk.yellow(hour.condition.description)}`, + ); + console.log( + `🌡️ Temp: ${hour.temperature}°C (feels like ${hour.feelsLike}°C)`, + ); + console.log( + `💧 Humidity: ${hour.humidity}% | ☁️ Clouds: ${hour.cloudiness}% | 🌦️ Rain: ${hour.rainLastHour ?? 0} mm`, + ); + console.log(`💨 Wind: ${hour.windSpeed} m/s, ${hour.windDirection}°`); + console.log(`🌓 Part of Day: ${hour.partOfDay}\n`); + }); +}; diff --git a/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/tsconfig.json b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/tsconfig.json new file mode 100644 index 0000000..542b8d8 --- /dev/null +++ b/web/Learning-Season/03-backend/challenges/ode.js & Modern JavaScript/submission/tsconfig.json @@ -0,0 +1,43 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + "rootDir": "./src", + "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + // For nodejs: + "lib": ["esnext"], + "types": ["node"], + // and npm install -D @types/node + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true + } +}