From ae2b3b106ab63be56cafba1f36707423862e4061 Mon Sep 17 00:00:00 2001 From: Alexis Bruneteau Date: Sat, 31 May 2025 02:43:51 +0200 Subject: [PATCH] waouh --- .gitea/workflows/deploy.yml | 69 ++ .postcssrc.json | 12 +- Dockerfile | 14 + angular.json | 4 +- deploy/k8s/deployment.yaml | 32 + deploy/k8s/ingress.yaml | 19 + deploy/k8s/namespace.yaml | 4 + deploy/k8s/service.yaml | 12 + deploy/nginx.conf | 15 + package-lock.json | 737 ++++++++++++++++-- package.json | 5 + src/app/app.css | 1 + src/app/app.html | 335 -------- src/app/app.routes.ts | 16 +- src/app/components/login/login.component.css | 0 src/app/components/login/login.component.html | 121 +++ .../components/login/login.component.spec.ts | 23 + src/app/components/login/login.component.ts | 190 +++++ .../register/register.component.css | 0 .../register/register.component.html | 137 ++++ .../register/register.component.spec.ts | 23 + .../components/register/register.component.ts | 167 ++++ src/app/services/api.spec.ts | 16 + src/app/services/api.ts | 208 +++++ src/environments/environment.ts | 4 + src/index.html | 2 +- src/main.ts | 18 +- src/styles.css | 1 + 28 files changed, 1788 insertions(+), 397 deletions(-) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 Dockerfile create mode 100644 deploy/k8s/deployment.yaml create mode 100644 deploy/k8s/ingress.yaml create mode 100644 deploy/k8s/namespace.yaml create mode 100644 deploy/k8s/service.yaml create mode 100644 deploy/nginx.conf create mode 100644 src/app/components/login/login.component.css create mode 100644 src/app/components/login/login.component.html create mode 100644 src/app/components/login/login.component.spec.ts create mode 100644 src/app/components/login/login.component.ts create mode 100644 src/app/components/register/register.component.css create mode 100644 src/app/components/register/register.component.html create mode 100644 src/app/components/register/register.component.spec.ts create mode 100644 src/app/components/register/register.component.ts create mode 100644 src/app/services/api.spec.ts create mode 100644 src/app/services/api.ts diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..062b4c9 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,69 @@ +name: Build and Deploy to k3s +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build Angular app + run: npm run build --prod + + - name: Build Docker image + run: | + docker build -t ${{ secrets.REGISTRY_URL }}/angular-app:${{ github.sha }} . + docker tag ${{ secrets.REGISTRY_URL }}/angular-app:${{ github.sha }} ${{ secrets.REGISTRY_URL }}/angular-app:latest + + - name: Login to Container Registry + run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_URL }} -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + + - name: Push Docker image + run: | + docker push ${{ secrets.REGISTRY_URL }}/angular-app:${{ github.sha }} + docker push ${{ secrets.REGISTRY_URL }}/angular-app:latest + + - name: Setup kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'latest' + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Create ConfigMap for app files + run: | + kubectl create configmap angular-app-files \ + --from-file=dist/your-angular-app/ \ + --namespace=angular-app \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Create ConfigMap for nginx config + run: | + kubectl create configmap nginx-config \ + --from-file=nginx.conf \ + --namespace=angular-app \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Deploy to k3s + run: | + kubectl apply -f k8s/ + kubectl set image deployment/angular-app angular-app=${{ secrets.REGISTRY_URL }}/angular-app:${{ github.sha }} -n angular-app + kubectl rollout status deployment/angular-app -n angular-app diff --git a/.postcssrc.json b/.postcssrc.json index c21c076..e092dc7 100644 --- a/.postcssrc.json +++ b/.postcssrc.json @@ -1,7 +1,5 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; - +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..afd3a1c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# Build stage +FROM node:22-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +RUN npm run build --prod + +# Production stage +FROM nginx:alpine AS prod +COPY --from=build /app/dist/hosting-frontend/browser /usr/share/nginx/html +COPY ./deploy/nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/angular.json b/angular.json index 9836708..9ec2f1e 100644 --- a/angular.json +++ b/angular.json @@ -38,8 +38,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "16kB", + "maximumError": "32kB" } ], "outputHashing": "all" diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/deployment.yaml new file mode 100644 index 0000000..f7ceac1 --- /dev/null +++ b/deploy/k8s/deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hosting-frontend + namespace: hosting +spec: + replicas: 2 + selector: + matchLabels: + app: hosting-frontend + template: + metadata: + labels: + app: hosting-frontend + spec: + containers: + - name: hosting-frontend + image: nginx:alpine + ports: + - containerPort: 80 + volumeMounts: + - name: app-volume + mountPath: /usr/share/nginx/html + - name: nginx-config + mountPath: /etc/nginx/conf.d + volumes: + - name: app-volume + configMap: + name: hosting-files + - name: nginx-config + configMap: + name: nginx-config diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/ingress.yaml new file mode 100644 index 0000000..81fd700 --- /dev/null +++ b/deploy/k8s/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: hosting-ingress + namespace: hosting + annotations: + kubernetes.io/ingress.class: traefik +spec: + rules: + - host: hosting.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: hosting-frontend-service + port: + number: 80 diff --git a/deploy/k8s/namespace.yaml b/deploy/k8s/namespace.yaml new file mode 100644 index 0000000..d038236 --- /dev/null +++ b/deploy/k8s/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: hosting diff --git a/deploy/k8s/service.yaml b/deploy/k8s/service.yaml new file mode 100644 index 0000000..d6aaff2 --- /dev/null +++ b/deploy/k8s/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: hosting-frontend-service + namespace: hosting +spec: + selector: + app: hosting-frontend + ports: + - port: 80 + targetPort: 80 + type: ClusterIP diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..a9e1bd6 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/package-lock.json b/package-lock.json index 727ddff..1573b75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", + "@tailwindcss/cli": "^4.1.8", + "@tailwindcss/postcss": "^4.1.8", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -23,20 +25,34 @@ "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "@types/jasmine": "~5.1.0", + "autoprefixer": "^10.4.21", "jasmine-core": "~5.7.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.4", + "tailwindcss": "^4.1.8", "typescript": "~5.8.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==", + "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", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1486,7 +1502,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -1509,7 +1524,6 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -1524,7 +1538,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1534,7 +1547,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1544,14 +1556,12 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2364,10 +2374,8 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, "hasInstallScript": true, "license": "MIT", - "optional": true, "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", @@ -2404,7 +2412,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2425,7 +2432,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2446,7 +2452,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2467,7 +2472,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2488,7 +2492,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2509,7 +2512,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2530,7 +2532,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2551,7 +2552,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2572,7 +2572,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2593,7 +2592,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2614,7 +2612,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2635,7 +2632,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2656,7 +2652,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2674,9 +2669,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, "license": "Apache-2.0", - "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -2688,9 +2681,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -3087,6 +3078,335 @@ "dev": true, "license": "MIT" }, + "node_modules/@tailwindcss/cli": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.8.tgz", + "integrity": "sha512-+6lkjXSr/68zWiabK3mVYVHmOq/SAHjJ13mR8spyB4LgUWZbWzU9kCSErlAUo+gK5aVfgqe8kY6Ltz9+nz5XYA==", + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.1.8", + "@tailwindcss/oxide": "4.1.8", + "enhanced-resolve": "^5.18.1", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.1.8" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz", + "integrity": "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.8" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.8.tgz", + "integrity": "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.8", + "@tailwindcss/oxide-darwin-arm64": "4.1.8", + "@tailwindcss/oxide-darwin-x64": "4.1.8", + "@tailwindcss/oxide-freebsd-x64": "4.1.8", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", + "@tailwindcss/oxide-linux-x64-musl": "4.1.8", + "@tailwindcss/oxide-wasm32-wasi": "4.1.8", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.8.tgz", + "integrity": "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.8.tgz", + "integrity": "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.8.tgz", + "integrity": "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.8.tgz", + "integrity": "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.8.tgz", + "integrity": "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.8.tgz", + "integrity": "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.8.tgz", + "integrity": "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.8.tgz", + "integrity": "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.8.tgz", + "integrity": "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.8.tgz", + "integrity": "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.10", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz", + "integrity": "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.8.tgz", + "integrity": "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.8.tgz", + "integrity": "sha512-vB/vlf7rIky+w94aWMw34bWW1ka6g6C3xIOdICKX2GC0VcLtL6fhlLiafF0DVIwa9V6EHz8kbWMkS2s2QvvNlw==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.8", + "@tailwindcss/oxide": "4.1.8", + "postcss": "^8.4.41", + "tailwindcss": "4.1.8" + } + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -3339,6 +3659,44 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3453,7 +3811,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4131,9 +4488,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -4344,6 +4699,19 @@ } } }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/ent": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", @@ -4573,7 +4941,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4676,6 +5043,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -4877,7 +5258,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -5214,7 +5594,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5237,7 +5616,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5263,7 +5641,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5425,6 +5802,15 @@ "dev": true, "license": "MIT" }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5839,6 +6225,234 @@ "node": ">=10" } }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/listr2": { "version": "8.3.3", "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", @@ -6065,7 +6679,6 @@ "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -6134,9 +6747,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -6149,9 +6760,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=8.6" }, @@ -6235,7 +6844,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -6375,7 +6983,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -6397,6 +7004,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -6462,7 +7078,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -6649,6 +7264,16 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-bundled": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", @@ -7051,7 +7676,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7084,7 +7708,6 @@ "version": "8.5.4", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7116,6 +7739,13 @@ "dev": true, "license": "MIT" }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", @@ -7734,7 +8364,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7995,6 +8624,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwindcss": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz", + "integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -8130,7 +8774,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" diff --git a/package.json b/package.json index 4747b53..b0a1a07 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", + "@tailwindcss/cli": "^4.1.8", + "@tailwindcss/postcss": "^4.1.8", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -25,12 +27,15 @@ "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", "@types/jasmine": "~5.1.0", + "autoprefixer": "^10.4.21", "jasmine-core": "~5.7.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.4", + "tailwindcss": "^4.1.8", "typescript": "~5.8.2" } } diff --git a/src/app/app.css b/src/app/app.css index e69de29..f1d8c73 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/src/app/app.html b/src/app/app.html index 36093e1..67e7bd4 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,336 +1 @@ - - - - - - - - - - - -
-
-
- -

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 }} - - - - - } -
- -
-
-
- - - - - - - - - - diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb..741343c 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,17 @@ import { Routes } from '@angular/router'; +import {LoginComponent} from './components/login/login.component'; +import {RegisterComponent} from './components/register/register.component'; +export const routes: Routes = [ -export const routes: Routes = []; + { + path: 'login', + component: LoginComponent, + title: 'Login - Your App Name' + }, + + { + path: 'register', + component: RegisterComponent, + title: 'Register - Your App Name' + }, +]; diff --git a/src/app/components/login/login.component.css b/src/app/components/login/login.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/login/login.component.html b/src/app/components/login/login.component.html new file mode 100644 index 0000000..0bdb05a --- /dev/null +++ b/src/app/components/login/login.component.html @@ -0,0 +1,121 @@ +
+
+ +
+

Welcome Back

+

Please sign in to your account

+
+ + + @if (success) { + + } + + + @if (error) { + + } + + +
+ + +
+ + + @if (hasFieldError('email')) { +

{{ getFieldError('email') }}

+ } +
+ + +
+ + + + @if (hasFieldError('password')) { +

{{ getFieldError('password') }}

+ } +
+ + +
+ + +
+ + + +
+ + +
+ Don't have an account? + +
+
+
diff --git a/src/app/components/login/login.component.spec.ts b/src/app/components/login/login.component.spec.ts new file mode 100644 index 0000000..01a2c7f --- /dev/null +++ b/src/app/components/login/login.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {LoginComponent} from './login.component'; + +describe('Login', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts new file mode 100644 index 0000000..ddb7205 --- /dev/null +++ b/src/app/components/login/login.component.ts @@ -0,0 +1,190 @@ +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; +import {Router, ActivatedRoute} from '@angular/router'; +import {Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; +import {ApiService, LoginCredentials} from '../../services/api'; +import {NgClass} from '@angular/common'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + imports: [ + ReactiveFormsModule, + NgClass + ], + styleUrls: ['./login.component.css'] +}) +export class LoginComponent implements OnInit, OnDestroy { + loginForm: FormGroup; + loading = false; + error = ''; + success = ''; + showPassword = false; + returnUrl = ''; + + private destroy$ = new Subject(); + + constructor( + private formBuilder: FormBuilder, + private apiService: ApiService, + private router: Router, + private route: ActivatedRoute + ) { + // Redirect to dashboard if already logged in + if (this.apiService.isAuthenticated) { + this.router.navigate(['/dashboard']); + } + + this.loginForm = this.formBuilder.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(6)]], + rememberMe: [false] + }); + } + + ngOnInit(): void { + // Get return url from route parameters or default to dashboard + this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard'; + + // Clear any existing error messages + this.clearMessages(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + // Getter for easy access to form fields + get f() { + return this.loginForm.controls; + } + + // Get specific field error message + getFieldError(fieldName: string): string { + const field = this.loginForm.get(fieldName); + if (field?.errors && field.touched) { + if (field.errors['required']) { + return `${this.capitalizeFirst(fieldName)} is required`; + } + if (field.errors['email']) { + return 'Please enter a valid email address'; + } + if (field.errors['minlength']) { + return `${this.capitalizeFirst(fieldName)} must be at least ${field.errors['minlength'].requiredLength} characters`; + } + } + return ''; + } + + // Check if field has error + hasFieldError(fieldName: string): boolean { + const field = this.loginForm.get(fieldName); + return !!(field?.errors && field.touched); + } + + // Capitalize first letter + private capitalizeFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + // Toggle password visibility + togglePasswordVisibility(): void { + this.showPassword = !this.showPassword; + } + + // Clear error and success messages + clearMessages(): void { + this.error = ''; + this.success = ''; + } + + // Handle form submission + onSubmit(): void { + this.clearMessages(); + + // Mark all fields as touched to show validation errors + this.loginForm.markAllAsTouched(); + + if (this.loginForm.invalid) { + return; + } + + this.loading = true; + + const credentials: LoginCredentials = { + email: this.f['email'].value, + password: this.f['password'].value + }; + + this.apiService.login(credentials) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response) => { + this.loading = false; + + if (response.success) { + this.success = 'Login successful! Redirecting...'; + + // Store remember me preference if needed + if (this.f['rememberMe'].value) { + localStorage.setItem('rememberMe', 'true'); + } + + // Redirect after a short delay to show success message + setTimeout(() => { + this.router.navigate([this.returnUrl]); + }, 1000); + + } else { + this.error = response.message || 'Login failed. Please try again.'; + } + }, + error: (error) => { + this.loading = false; + + // Handle specific error cases + if (error.status === 422 && error.error?.errors) { + // Validation errors from server + const serverErrors = error.error.errors; + let errorMessages: string[] = []; + + Object.keys(serverErrors).forEach(key => { + if (Array.isArray(serverErrors[key])) { + errorMessages = errorMessages.concat(serverErrors[key]); + } + }); + + this.error = errorMessages.join(', '); + } else if (error.status === 401) { + this.error = 'Invalid email or password. Please try again.'; + } else { + this.error = error.userMessage || 'An error occurred. Please try again.'; + } + + console.error('Login error:', error); + } + }); + } + + // Navigate to register page + goToRegister(): void { + this.router.navigate(['/register'], { + queryParams: {returnUrl: this.returnUrl} + }); + } + + // Navigate to forgot password page + goToForgotPassword(): void { + this.router.navigate(['/forgot-password']); + } + + // Demo login (optional - for testing) + demoLogin(): void { + this.loginForm.patchValue({ + email: 'demo@example.com', + password: 'password123' + }); + } +} diff --git a/src/app/components/register/register.component.css b/src/app/components/register/register.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/register/register.component.html b/src/app/components/register/register.component.html new file mode 100644 index 0000000..4a601be --- /dev/null +++ b/src/app/components/register/register.component.html @@ -0,0 +1,137 @@ +
+
+ +
+

Create your account

+

Sign up to get started!

+
+ + + @if (success) { + + } + + + + @if (error) { + + } + + +
+ +
+ + + @if (hasFieldError('name')) { +

{{ getFieldError('name') }}

+ } +
+ + +
+ + + @if (hasFieldError('email')) { +

{{ getFieldError('email') }}

+ } +
+ + +
+ + + @if (hasFieldError('password')) { +

{{ getFieldError('password') }}

+ } +
+ + +
+ + + @if (hasFieldError('password_confirmation')) { +

{{ getFieldError('password_confirmation') }}

+ } +
+ + + +
+ + +
+ Already have an account? + +
+
+
diff --git a/src/app/components/register/register.component.spec.ts b/src/app/components/register/register.component.spec.ts new file mode 100644 index 0000000..642b0c9 --- /dev/null +++ b/src/app/components/register/register.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegisterComponent } from './register.component'; + +describe('Register', () => { + let component: RegisterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegisterComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts new file mode 100644 index 0000000..ad2ebaa --- /dev/null +++ b/src/app/components/register/register.component.ts @@ -0,0 +1,167 @@ +import {Component, OnInit, OnDestroy} from '@angular/core'; +import { + FormBuilder, + FormGroup, + Validators, + AbstractControl, + ValidationErrors, + ReactiveFormsModule +} from '@angular/forms'; +import {Router, ActivatedRoute} from '@angular/router'; +import {Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; +import {ApiResponse, ApiService, AuthResponse, RegisterData} from '../../services/api'; +import {NgClass} from '@angular/common'; + +@Component({ + selector: 'app-register', + templateUrl: './register.component.html', + imports: [ + ReactiveFormsModule, + NgClass + ], + styleUrls: ['./register.component.css'] +}) +export class RegisterComponent implements OnInit, OnDestroy { + registerForm: FormGroup; + loading = false; + error = ''; + success = ''; + private destroy$ = new Subject(); + returnUrl = ''; + + constructor( + private fb: FormBuilder, + private apiService: ApiService, + private router: Router, + private route: ActivatedRoute + ) { + this.registerForm = this.fb.group({ + name: ['', [Validators.required, Validators.minLength(3)]], + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(6)]], + password_confirmation: ['', [Validators.required]] + }, {validators: this.passwordsMatchValidator}); + } + + ngOnInit(): void { + this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard'; + this.clearMessages(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + // Custom validator to check password and confirmation match + passwordsMatchValidator(group: AbstractControl): ValidationErrors | null { + const password = group.get('password')?.value; + const confirm = group.get('password_confirmation')?.value; + return password === confirm ? null : {passwordsMismatch: true}; + } + + get f() { + return this.registerForm.controls; + } + + getFieldError(fieldName: string): string { + const field = this.registerForm.get(fieldName); + if (!field) return ''; + + if (field.errors && (field.touched || field.dirty)) { + if (field.errors['required']) { + return `${this.capitalizeFirst(fieldName.replace('_', ' '))} is required`; + } + if (field.errors['email']) { + return 'Please enter a valid email address'; + } + if (field.errors['minlength']) { + return `${this.capitalizeFirst(fieldName.replace('_', ' '))} must be at least ${field.errors['minlength'].requiredLength} characters`; + } + } + + if (fieldName === 'password_confirmation' && this.registerForm.errors?.["passwordsMismatch"] && (field.touched || field.dirty)) { + return 'Passwords do not match'; + } + + return ''; + } + + hasFieldError(fieldName: string): boolean { + const field = this.registerForm.get(fieldName); + if (!field) return false; + + if (fieldName === 'password_confirmation') { + return field.touched && this.registerForm.errors?.["passwordsMismatch"] === true; + } + + return !!(field.errors && (field.touched || field.dirty)); + } + + private capitalizeFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + clearMessages(): void { + this.error = ''; + this.success = ''; + } + + onSubmit(): void { + this.clearMessages(); + this.registerForm.markAllAsTouched(); + + if (this.registerForm.invalid) { + return; + } + + this.loading = true; + + const userData: RegisterData = { + name: this.f['name'].value, + email: this.f['email'].value, + password: this.f['password'].value, + password_confirmation: this.f['password_confirmation'].value + }; + + + this.apiService.register(userData) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (response: ApiResponse) => { + this.loading = false; + if (response.success) { + this.success = 'Registration successful! Redirecting...'; + setTimeout(() => { + this.router.navigate([this.returnUrl]); + }, 1000); + } else { + this.error = response.message || 'Registration failed. Please try again.'; + } + }, + error: (error: { status: number; error: { errors: any; }; userMessage: string; }) => { + this.loading = false; + if (error.status === 422 && error.error?.errors) { + const serverErrors = error.error.errors; + let errorMessages: string[] = []; + + Object.keys(serverErrors).forEach(key => { + if (Array.isArray(serverErrors[key])) { + errorMessages = errorMessages.concat(serverErrors[key]); + } + }); + + this.error = errorMessages.join(', '); + } else { + this.error = error.userMessage || 'An error occurred. Please try again.'; + } + console.error('Registration error:', error); + } + }); + } + + goToLogin(): void { + this.router.navigate(['/login'], {queryParams: {returnUrl: this.returnUrl}}); + } +} diff --git a/src/app/services/api.spec.ts b/src/app/services/api.spec.ts new file mode 100644 index 0000000..5824fb1 --- /dev/null +++ b/src/app/services/api.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { Api } from './api'; + +describe('Api', () => { + let service: Api; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(Api); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/api.ts b/src/app/services/api.ts new file mode 100644 index 0000000..20f011b --- /dev/null +++ b/src/app/services/api.ts @@ -0,0 +1,208 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; +import { Observable, throwError, BehaviorSubject } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; +import { environment as env } from '../../environments/environment' ; + +export interface LoginCredentials { + email: string; + password: string; +} + +export interface RegisterData { + name: string; + email: string; + password: string; + password_confirmation: string; +} + +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + errors?: any; +} + +export interface AuthResponse { + user: any; + token: string; + expires_in?: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class ApiService { + private baseUrl = env.apiUrl || 'http://localhost:8000/api'; + private tokenSubject = new BehaviorSubject(this.getStoredToken()); + public token$ = this.tokenSubject.asObservable(); + + constructor(private http: HttpClient) {} + + // Get stored token from localStorage + private getStoredToken(): string | null { + if (typeof window !== 'undefined') { + return localStorage.getItem('auth_token'); + } + return null; + } + + // Set token in localStorage and update subject + private setToken(token: string): void { + if (typeof window !== 'undefined') { + localStorage.setItem('auth_token', token); + } + this.tokenSubject.next(token); + } + + // Remove token from localStorage and update subject + private removeToken(): void { + if (typeof window !== 'undefined') { + localStorage.removeItem('auth_token'); + } + this.tokenSubject.next(null); + } + + // Get current token value + get currentToken(): string | null { + return this.tokenSubject.value; + } + + // Check if user is authenticated + get isAuthenticated(): boolean { + return !!this.currentToken; + } + + // Get HTTP headers with auth token + private getHeaders(): HttpHeaders { + let headers = new HttpHeaders({ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }); + + const token = this.currentToken; + if (token) { + headers = headers.set('Authorization', `Bearer ${token}`); + } + + return headers; + } + + // Handle HTTP errors + private handleError(error: HttpErrorResponse): Observable { + let errorMessage = 'An unknown error occurred'; + + if (error.error instanceof ErrorEvent) { + // Client-side error + errorMessage = error.error.message; + } else { + // Server-side error + if (error.status === 401) { + this.removeToken(); + errorMessage = 'Unauthorized. Please login again.'; + } else if (error.status === 422) { + errorMessage = 'Validation error'; + } else if (error.error?.message) { + errorMessage = error.error.message; + } else { + errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`; + } + } + + console.error('API Error:', error); + return throwError(() => ({ ...error, userMessage: errorMessage })); + } + + // Generic HTTP methods + private get(endpoint: string): Observable { + return this.http.get(`${this.baseUrl}${endpoint}`, { + headers: this.getHeaders() + }).pipe( + catchError(this.handleError.bind(this)) + ); + } + + private post(endpoint: string, data: any): Observable { + return this.http.post(`${this.baseUrl}${endpoint}`, data, { + headers: this.getHeaders() + }).pipe( + catchError(this.handleError.bind(this)) + ); + } + + private put(endpoint: string, data: any): Observable { + return this.http.put(`${this.baseUrl}${endpoint}`, data, { + headers: this.getHeaders() + }).pipe( + catchError(this.handleError.bind(this)) + ); + } + + private delete(endpoint: string): Observable { + return this.http.delete(`${this.baseUrl}${endpoint}`, { + headers: this.getHeaders() + }).pipe( + catchError(this.handleError.bind(this)) + ); + } + + // Authentication methods + register(userData: RegisterData): Observable> { + return this.post>('/auth/register', userData).pipe( + tap(response => { + if (response.success && response.data?.token) { + this.setToken(response.data.token); + } + }) + ); + } + + login(credentials: LoginCredentials): Observable> { + return this.post>('/auth/login', credentials).pipe( + tap(response => { + if (response.success && response.data?.token) { + this.setToken(response.data.token); + } + }) + ); + } + + logout(): Observable { + return this.post('/auth/logout', {}).pipe( + tap(() => { + this.removeToken(); + }) + ); + } + + getCurrentUser(): Observable { + return this.get('/auth/user'); + } + + refreshToken(): Observable> { + return this.post>('/auth/refresh', {}).pipe( + tap(response => { + if (response.success && response.data?.token) { + this.setToken(response.data.token); + } + }) + ); + } + + // Generic API methods for future use + getData(endpoint: string): Observable { + return this.get(endpoint); + } + + postData(endpoint: string, data: any): Observable { + return this.post(endpoint, data); + } + + putData(endpoint: string, data: any): Observable { + return this.put(endpoint, data); + } + + deleteData(endpoint: string): Observable { + return this.delete(endpoint); + } +} diff --git a/src/environments/environment.ts b/src/environments/environment.ts index e69de29..b0f3674 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + apiUrl: 'http://localhost:8000/api' +}; diff --git a/src/index.html b/src/index.html index dac9d34..b43e1c0 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,6 @@ - + diff --git a/src/main.ts b/src/main.ts index 5df75f9..3846c7d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,16 @@ import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { App } from './app/app'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { importProvidersFrom } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; -bootstrapApplication(App, appConfig) - .catch((err) => console.error(err)); +import { App } from './app/app'; +import { routes } from './app/app.routes'; + +bootstrapApplication(App, { + providers: [ + provideRouter(routes), + provideHttpClient(), + importProvidersFrom(ReactiveFormsModule) + ] +}).catch(err => console.error(err)); diff --git a/src/styles.css b/src/styles.css index 90d4ee0..ee2fdbb 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,2 @@ +@import "tailwindcss"; /* You can add global styles to this file, and also import other style files */