From f0f174c384bb200bd5d724384fe7d7359aba7683 Mon Sep 17 00:00:00 2001 From: Bach Nguyen Date: Thu, 5 Dec 2024 15:50:46 +0700 Subject: [PATCH 1/6] feat: add docker compose config for db --- backend/pyproject.toml | 2 +- docker-compose.yml | 159 +------------ frontend/package-lock.json | 477 ++++++++++++++++++++----------------- 3 files changed, 258 insertions(+), 380 deletions(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1c77b83ded..32a74007b6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "alembic<2.0.0,>=1.12.1", "httpx<1.0.0,>=0.25.1", "psycopg[binary]<4.0.0,>=3.1.13", - "sqlmodel<1.0.0,>=0.0.21", + "sqlmodel>=0.0.21,<1.0.0", # Pin bcrypt until passlib supports the latest "bcrypt==4.0.1", "pydantic-settings<3.0.0,>=2.2.1", diff --git a/docker-compose.yml b/docker-compose.yml index c92d5d4451..2388a8a93d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,6 @@ services: db: image: postgres:12 restart: always - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] - interval: 10s - retries: 5 - start_period: 30s - timeout: 10s volumes: - app-db-data:/var/lib/postgresql/data/pgdata env_file: @@ -18,154 +12,9 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - POSTGRES_USER=${POSTGRES_USER?Variable not set} - POSTGRES_DB=${POSTGRES_DB?Variable not set} + ports: + - "5432:5432" - adminer: - image: adminer - restart: always - networks: - - traefik-public - - default - depends_on: - - db - environment: - - ADMINER_DESIGN=pepa-linha-dark - labels: - - traefik.enable=true - - traefik.docker.network=traefik-public - - traefik.constraint-label=traefik-public - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.rule=Host(`adminer.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.entrypoints=http - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.middlewares=https-redirect - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.rule=Host(`adminer.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.entrypoints=https - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls=true - - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls.certresolver=le - - traefik.http.services.${STACK_NAME?Variable not set}-adminer.loadbalancer.server.port=8080 - - prestart: - image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' - build: - context: ./backend - networks: - - traefik-public - - default - depends_on: - db: - condition: service_healthy - restart: true - command: bash scripts/prestart.sh - env_file: - - .env - environment: - - DOMAIN=${DOMAIN} - - FRONTEND_HOST=${FRONTEND_HOST?Variable not set} - - ENVIRONMENT=${ENVIRONMENT} - - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} - - SECRET_KEY=${SECRET_KEY?Variable not set} - - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} - - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} - - SMTP_HOST=${SMTP_HOST} - - SMTP_USER=${SMTP_USER} - - SMTP_PASSWORD=${SMTP_PASSWORD} - - EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL} - - POSTGRES_SERVER=db - - POSTGRES_PORT=${POSTGRES_PORT} - - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER?Variable not set} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - - SENTRY_DSN=${SENTRY_DSN} - - backend: - image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' - restart: always - networks: - - traefik-public - - default - depends_on: - db: - condition: service_healthy - restart: true - prestart: - condition: service_completed_successfully - env_file: - - .env - environment: - - DOMAIN=${DOMAIN} - - FRONTEND_HOST=${FRONTEND_HOST?Variable not set} - - ENVIRONMENT=${ENVIRONMENT} - - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} - - SECRET_KEY=${SECRET_KEY?Variable not set} - - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} - - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} - - SMTP_HOST=${SMTP_HOST} - - SMTP_USER=${SMTP_USER} - - SMTP_PASSWORD=${SMTP_PASSWORD} - - EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL} - - POSTGRES_SERVER=db - - POSTGRES_PORT=${POSTGRES_PORT} - - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER?Variable not set} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - - SENTRY_DSN=${SENTRY_DSN} - - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health-check/"] - interval: 10s - timeout: 5s - retries: 5 - - build: - context: ./backend - labels: - - traefik.enable=true - - traefik.docker.network=traefik-public - - traefik.constraint-label=traefik-public - - - traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=8000 - - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=Host(`api.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.entrypoints=http - - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=Host(`api.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.entrypoints=https - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le - - # Enable redirection for HTTP and HTTPS - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect - - frontend: - image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${TAG-latest}' - restart: always - networks: - - traefik-public - - default - build: - context: ./frontend - args: - - VITE_API_URL=https://api.${DOMAIN?Variable not set} - - NODE_ENV=production - labels: - - traefik.enable=true - - traefik.docker.network=traefik-public - - traefik.constraint-label=traefik-public - - - traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80 - - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=Host(`dashboard.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.entrypoints=http - - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.rule=Host(`dashboard.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.entrypoints=https - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls=true - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls.certresolver=le - - # Enable redirection for HTTP and HTTPS - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect + volumes: - app-db-data: - -networks: - traefik-public: - # Allow setting it to false for testing - external: true + app-db-data: \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 661ce88605..b1d42d6704 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1729,10 +1729,26 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", - "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -1746,9 +1762,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", - "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -1762,9 +1778,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", - "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -1778,9 +1794,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", - "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -1794,9 +1810,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", - "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -1810,9 +1826,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", - "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -1826,9 +1842,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", - "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -1842,9 +1858,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", - "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -1858,9 +1874,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", - "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -1874,9 +1890,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", - "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -1890,9 +1906,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", - "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -1906,9 +1922,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", - "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -1922,9 +1938,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", - "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -1938,9 +1954,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", - "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -1954,9 +1970,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", - "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -1970,9 +1986,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", - "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -1986,9 +2002,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", - "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -2002,9 +2018,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", - "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -2018,9 +2034,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", - "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -2034,9 +2050,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", - "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -2050,9 +2066,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", - "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -2066,9 +2082,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", - "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -3115,9 +3131,9 @@ } }, "node_modules/esbuild": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", - "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -3127,28 +3143,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.19.8", - "@esbuild/android-arm64": "0.19.8", - "@esbuild/android-x64": "0.19.8", - "@esbuild/darwin-arm64": "0.19.8", - "@esbuild/darwin-x64": "0.19.8", - "@esbuild/freebsd-arm64": "0.19.8", - "@esbuild/freebsd-x64": "0.19.8", - "@esbuild/linux-arm": "0.19.8", - "@esbuild/linux-arm64": "0.19.8", - "@esbuild/linux-ia32": "0.19.8", - "@esbuild/linux-loong64": "0.19.8", - "@esbuild/linux-mips64el": "0.19.8", - "@esbuild/linux-ppc64": "0.19.8", - "@esbuild/linux-riscv64": "0.19.8", - "@esbuild/linux-s390x": "0.19.8", - "@esbuild/linux-x64": "0.19.8", - "@esbuild/netbsd-x64": "0.19.8", - "@esbuild/openbsd-x64": "0.19.8", - "@esbuild/sunos-x64": "0.19.8", - "@esbuild/win32-arm64": "0.19.8", - "@esbuild/win32-ia32": "0.19.8", - "@esbuild/win32-x64": "0.19.8" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escape-string-regexp": { @@ -3658,9 +3675,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -3835,9 +3852,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/pkg-types": { @@ -3897,9 +3914,9 @@ } }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -3917,8 +3934,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4265,9 +4282,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "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, "engines": { "node": ">=0.10.0" @@ -4437,14 +4454,14 @@ } }, "node_modules/vite": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.13.tgz", - "integrity": "sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -4463,6 +4480,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -4480,6 +4498,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -5794,157 +5815,164 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true + }, "@esbuild/android-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", - "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", - "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", - "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", - "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", - "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", - "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", - "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", - "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", - "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", - "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", - "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", - "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", - "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", - "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", - "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", - "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", - "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", - "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", - "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", - "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", - "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", - "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "dev": true, "optional": true }, @@ -6602,33 +6630,34 @@ } }, "esbuild": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", - "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.19.8", - "@esbuild/android-arm64": "0.19.8", - "@esbuild/android-x64": "0.19.8", - "@esbuild/darwin-arm64": "0.19.8", - "@esbuild/darwin-x64": "0.19.8", - "@esbuild/freebsd-arm64": "0.19.8", - "@esbuild/freebsd-x64": "0.19.8", - "@esbuild/linux-arm": "0.19.8", - "@esbuild/linux-arm64": "0.19.8", - "@esbuild/linux-ia32": "0.19.8", - "@esbuild/linux-loong64": "0.19.8", - "@esbuild/linux-mips64el": "0.19.8", - "@esbuild/linux-ppc64": "0.19.8", - "@esbuild/linux-riscv64": "0.19.8", - "@esbuild/linux-s390x": "0.19.8", - "@esbuild/linux-x64": "0.19.8", - "@esbuild/netbsd-x64": "0.19.8", - "@esbuild/openbsd-x64": "0.19.8", - "@esbuild/sunos-x64": "0.19.8", - "@esbuild/win32-arm64": "0.19.8", - "@esbuild/win32-ia32": "0.19.8", - "@esbuild/win32-x64": "0.19.8" + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "escape-string-regexp": { @@ -6985,9 +7014,9 @@ } }, "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true }, "neo-async": { @@ -7101,9 +7130,9 @@ "dev": true }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "pkg-types": { @@ -7143,14 +7172,14 @@ "dev": true }, "postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "requires": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" } }, "prettier": { @@ -7371,9 +7400,9 @@ "dev": true }, "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "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 }, "strip-final-newline": { @@ -7480,15 +7509,15 @@ "requires": {} }, "vite": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.13.tgz", - "integrity": "sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, "requires": { - "esbuild": "^0.19.3", + "esbuild": "^0.21.3", "fsevents": "~2.3.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" } }, "which": { From c07b81c821f58b8004e3cc6d823302d27ee9b165 Mon Sep 17 00:00:00 2001 From: Bach Nguyen Date: Thu, 5 Dec 2024 16:27:41 +0700 Subject: [PATCH 2/6] feat: add new migration --- .../versions/80580dd7587b_add_three_tables.py | 47 +++++++++++++++++++ backend/app/models.py | 24 ++++++++++ 2 files changed, 71 insertions(+) create mode 100644 backend/app/alembic/versions/80580dd7587b_add_three_tables.py diff --git a/backend/app/alembic/versions/80580dd7587b_add_three_tables.py b/backend/app/alembic/versions/80580dd7587b_add_three_tables.py new file mode 100644 index 0000000000..393f57f625 --- /dev/null +++ b/backend/app/alembic/versions/80580dd7587b_add_three_tables.py @@ -0,0 +1,47 @@ +"""add three tables + +Revision ID: 80580dd7587b +Revises: 1a31ce608336 +Create Date: 2024-12-05 16:24:28.872367 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '80580dd7587b' +down_revision = '1a31ce608336' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('todo', + sa.Column('title', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False), + sa.Column('desc', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(length=20), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('subtodo', + sa.Column('title', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False), + sa.Column('desc', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('todo_id', sa.Uuid(), nullable=False), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(length=20), nullable=False), + sa.ForeignKeyConstraint(['todo_id'], ['todo.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('subtodo') + op.drop_table('todo') + # ### end Alembic commands ### diff --git a/backend/app/models.py b/backend/app/models.py index 90ef5559e3..21f8236324 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -112,3 +112,27 @@ class TokenPayload(SQLModel): class NewPassword(SQLModel): token: str new_password: str = Field(min_length=8, max_length=40) + +class TodoBase(SQLModel): + title: str = Field(min_length=1, max_length=255) + desc: str | None = Field(default=None, max_length=255) + +# Table Todo +class Todo(TodoBase, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + title: str = Field(max_length=255) + desc: str | None = Field(default=None, max_length=255) + user_id: uuid.UUID = Field( + foreign_key="user.id", nullable=False, ondelete="CASCADE" + ) + status: str = Field(max_length=20) + +# Table SubTodo +class SubTodo(TodoBase, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + title: str = Field(max_length=255) + desc: str | None = Field(default=None, max_length=255) + todo_id: uuid.UUID = Field( + foreign_key="todo.id", nullable=False, ondelete="CASCADE" + ) + status: str = Field(max_length=20) From 2913801d52d558dc5106103ba5f6a34fe79b238b Mon Sep 17 00:00:00 2001 From: bachnguyen-18 Date: Fri, 6 Dec 2024 11:04:14 +0700 Subject: [PATCH 3/6] update model for todo and subtodo table --- ...5f5673cb7_update_todo_and_subtodo_table.py | 63 ++++++++++++++++++ backend/app/models.py | 65 ++++++++++++++++--- 2 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 backend/app/alembic/versions/9245f5673cb7_update_todo_and_subtodo_table.py diff --git a/backend/app/alembic/versions/9245f5673cb7_update_todo_and_subtodo_table.py b/backend/app/alembic/versions/9245f5673cb7_update_todo_and_subtodo_table.py new file mode 100644 index 0000000000..9d97b31be6 --- /dev/null +++ b/backend/app/alembic/versions/9245f5673cb7_update_todo_and_subtodo_table.py @@ -0,0 +1,63 @@ +"""update todo and subtodo table + +Revision ID: 9245f5673cb7 +Revises: 80580dd7587b +Create Date: 2024-12-06 10:53:59.113398 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '9245f5673cb7' +down_revision = '80580dd7587b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('subtodo', 'desc', + existing_type=sa.VARCHAR(length=255), + nullable=False) + op.alter_column('subtodo', 'status', + existing_type=sa.VARCHAR(length=20), + type_=sqlmodel.sql.sqltypes.AutoString(length=255), + existing_nullable=False) + op.add_column('todo', sa.Column('owner_id', sa.Uuid(), nullable=False)) + op.alter_column('todo', 'desc', + existing_type=sa.VARCHAR(length=255), + nullable=False) + op.alter_column('todo', 'status', + existing_type=sa.VARCHAR(length=20), + type_=sqlmodel.sql.sqltypes.AutoString(length=255), + existing_nullable=False) + op.drop_constraint('todo_user_id_fkey', 'todo', type_='foreignkey') + op.create_foreign_key(None, 'todo', 'user', ['owner_id'], ['id'], ondelete='CASCADE') + op.drop_column('todo', 'user_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('todo', sa.Column('user_id', sa.UUID(), autoincrement=False, nullable=False)) + op.drop_constraint(None, 'todo', type_='foreignkey') + op.create_foreign_key('todo_user_id_fkey', 'todo', 'user', ['user_id'], ['id'], ondelete='CASCADE') + op.alter_column('todo', 'status', + existing_type=sqlmodel.sql.sqltypes.AutoString(length=255), + type_=sa.VARCHAR(length=20), + existing_nullable=False) + op.alter_column('todo', 'desc', + existing_type=sa.VARCHAR(length=255), + nullable=True) + op.drop_column('todo', 'owner_id') + op.alter_column('subtodo', 'status', + existing_type=sqlmodel.sql.sqltypes.AutoString(length=255), + type_=sa.VARCHAR(length=20), + existing_nullable=False) + op.alter_column('subtodo', 'desc', + existing_type=sa.VARCHAR(length=255), + nullable=True) + # ### end Alembic commands ### diff --git a/backend/app/models.py b/backend/app/models.py index 21f8236324..19bd64289e 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,4 +1,5 @@ import uuid +from enum import Enum from pydantic import EmailStr from sqlmodel import Field, Relationship, SQLModel @@ -44,6 +45,7 @@ class User(UserBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) hashed_password: str items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True) + todos: list["Todo"] = Relationship(back_populates="owner", cascade_delete=True) # Properties to return via API, id is always required @@ -113,26 +115,71 @@ class NewPassword(SQLModel): token: str new_password: str = Field(min_length=8, max_length=40) +# Shared properties +class StatusEnum(str, Enum): + pending = "pending" + completed = "completed" + in_progress = "in_progress" + class TodoBase(SQLModel): title: str = Field(min_length=1, max_length=255) - desc: str | None = Field(default=None, max_length=255) + desc: str = Field(max_length=255) # Table Todo class Todo(TodoBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - title: str = Field(max_length=255) - desc: str | None = Field(default=None, max_length=255) - user_id: uuid.UUID = Field( + owner_id: uuid.UUID = Field( foreign_key="user.id", nullable=False, ondelete="CASCADE" ) - status: str = Field(max_length=20) + status: str = Field(max_length=255) + owner: User | None = Relationship(back_populates="todos") + subtodos: list["SubTodo"] = Relationship(back_populates="todo") + +class TodoCreate(TodoBase): + pass + +# Properties to receive on item update +class TodoUpdate(TodoBase): + title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore + desc: str | None = Field(default=None, max_length=255) + status: str = Field(max_length=255) + +class TodoPublic(TodoBase): + id: uuid.UUID + owner_id: uuid.UUID + status: StatusEnum + +class TodosPublic(SQLModel): + data: list[TodoPublic] + count: int # Table SubTodo -class SubTodo(TodoBase, table=True): +class SubTodoBase(SQLModel): + title: str = Field(min_length=1, max_length=255) + desc: str = Field(max_length=255) + +class SubTodo(SubTodoBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - title: str = Field(max_length=255) - desc: str | None = Field(default=None, max_length=255) todo_id: uuid.UUID = Field( foreign_key="todo.id", nullable=False, ondelete="CASCADE" ) - status: str = Field(max_length=20) + status: str = Field(max_length=255) + todo: Todo | None = Relationship(back_populates="subtodos") + +class SubTodoCreate(SubTodoBase): + pass + +# Properties to receive on item update +class SubTodoUpdate(SubTodoBase): + title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore + desc: str | None = Field(default=None, max_length=255) + status: StatusEnum | None = Field(default=None) + +class SubTodoPublic(SubTodoBase): + id: uuid.UUID + todo_id: uuid.UUID + status: StatusEnum + +class SubTodosPublic(SQLModel): + data: list[SubTodoPublic] + count: int \ No newline at end of file From 19ad966182078b0fa13fd111c8bfceecf025ee85 Mon Sep 17 00:00:00 2001 From: blueoc-ducphong Date: Fri, 6 Dec 2024 13:45:38 +0700 Subject: [PATCH 4/6] feat: fix models --- backend/app/main.py | 5 ----- backend/app/models.py | 12 ++++++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 9a95801e74..2409721572 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,3 @@ -import sentry_sdk from fastapi import FastAPI from fastapi.routing import APIRoute from starlette.middleware.cors import CORSMiddleware @@ -10,10 +9,6 @@ def custom_generate_unique_id(route: APIRoute) -> str: return f"{route.tags[0]}-{route.name}" - -if settings.SENTRY_DSN and settings.ENVIRONMENT != "local": - sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True) - app = FastAPI( title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json", diff --git a/backend/app/models.py b/backend/app/models.py index 19bd64289e..908094bb75 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -131,7 +131,7 @@ class Todo(TodoBase, table=True): owner_id: uuid.UUID = Field( foreign_key="user.id", nullable=False, ondelete="CASCADE" ) - status: str = Field(max_length=255) + status: str = Field(default="in_progress", max_length=255) owner: User | None = Relationship(back_populates="todos") subtodos: list["SubTodo"] = Relationship(back_populates="todo") @@ -142,12 +142,12 @@ class TodoCreate(TodoBase): class TodoUpdate(TodoBase): title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore desc: str | None = Field(default=None, max_length=255) - status: str = Field(max_length=255) + status: str | None = Field(default=None, max_length=255) class TodoPublic(TodoBase): id: uuid.UUID owner_id: uuid.UUID - status: StatusEnum + status: str class TodosPublic(SQLModel): data: list[TodoPublic] @@ -163,7 +163,7 @@ class SubTodo(SubTodoBase, table=True): todo_id: uuid.UUID = Field( foreign_key="todo.id", nullable=False, ondelete="CASCADE" ) - status: str = Field(max_length=255) + status: str = Field(default="in_progress", max_length=255) todo: Todo | None = Relationship(back_populates="subtodos") class SubTodoCreate(SubTodoBase): @@ -173,12 +173,12 @@ class SubTodoCreate(SubTodoBase): class SubTodoUpdate(SubTodoBase): title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore desc: str | None = Field(default=None, max_length=255) - status: StatusEnum | None = Field(default=None) + status: str | None = Field(default=None, max_length=255) class SubTodoPublic(SubTodoBase): id: uuid.UUID todo_id: uuid.UUID - status: StatusEnum + status: str class SubTodosPublic(SQLModel): data: list[SubTodoPublic] From 79eabf6b51dfe14c1f50e0f0ff82e5dfadf30d25 Mon Sep 17 00:00:00 2001 From: bachnguyen-18 Date: Fri, 6 Dec 2024 16:55:16 +0700 Subject: [PATCH 5/6] feat: write api for sub todo --- ...odo_table.py => f2d6aba28d7d_refine_db.py} | 8 +- backend/app/api/main.py | 3 +- backend/app/api/routes/sub_todo.py | 149 ++++++++++++++++++ backend/app/crud.py | 9 +- backend/app/models.py | 15 +- 5 files changed, 171 insertions(+), 13 deletions(-) rename backend/app/alembic/versions/{9245f5673cb7_update_todo_and_subtodo_table.py => f2d6aba28d7d_refine_db.py} (94%) create mode 100644 backend/app/api/routes/sub_todo.py diff --git a/backend/app/alembic/versions/9245f5673cb7_update_todo_and_subtodo_table.py b/backend/app/alembic/versions/f2d6aba28d7d_refine_db.py similarity index 94% rename from backend/app/alembic/versions/9245f5673cb7_update_todo_and_subtodo_table.py rename to backend/app/alembic/versions/f2d6aba28d7d_refine_db.py index 9d97b31be6..0ccdd08446 100644 --- a/backend/app/alembic/versions/9245f5673cb7_update_todo_and_subtodo_table.py +++ b/backend/app/alembic/versions/f2d6aba28d7d_refine_db.py @@ -1,8 +1,8 @@ -"""update todo and subtodo table +"""refine db -Revision ID: 9245f5673cb7 +Revision ID: f2d6aba28d7d Revises: 80580dd7587b -Create Date: 2024-12-06 10:53:59.113398 +Create Date: 2024-12-06 14:59:08.495370 """ from alembic import op @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. -revision = '9245f5673cb7' +revision = 'f2d6aba28d7d' down_revision = '80580dd7587b' branch_labels = None depends_on = None diff --git a/backend/app/api/main.py b/backend/app/api/main.py index eac18c8e8f..22576359f9 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.routes import items, login, private, users, utils +from app.api.routes import items, login, private, users, utils, sub_todo from app.core.config import settings api_router = APIRouter() @@ -8,6 +8,7 @@ api_router.include_router(users.router) api_router.include_router(utils.router) api_router.include_router(items.router) +api_router.include_router(sub_todo.router) if settings.ENVIRONMENT == "local": diff --git a/backend/app/api/routes/sub_todo.py b/backend/app/api/routes/sub_todo.py new file mode 100644 index 0000000000..635479567b --- /dev/null +++ b/backend/app/api/routes/sub_todo.py @@ -0,0 +1,149 @@ +import uuid +from typing import Any + +from fastapi import APIRouter, HTTPException +from sqlmodel import func, select + +from app.api.deps import CurrentUser, SessionDep +from app.models import SubTodo, SubTodoCreate, SubTodoPublic, SubTodosPublic, SubTodoUpdate, Message, Todo + +router = APIRouter(prefix="/subtodos", tags=["subtodos"]) + + +@router.get("/", response_model=SubTodosPublic) +def read_sub_todos( + session: SessionDep, current_user: CurrentUser, todo_id=uuid.UUID, skip: int = 0, limit: int = 100 +) -> Any: + """ + Retrieve sub todos. + """ + + if current_user.is_superuser: + count_statement = select(func.count()).select_from(SubTodo) + count = session.exec(count_statement).one() + statement = select(SubTodo).offset(skip).limit(limit) + todos = session.exec(statement).all() + else: + count_statement = ( + select(func.count()) + .select_from(SubTodo) + .where(SubTodo.todo_id == todo_id) + ) + count = session.exec(count_statement).one() + statement = ( + select(SubTodo) + .where(SubTodo.todo_id == todo_id) + .offset(skip) + .limit(limit) + ) + todos = session.exec(statement).all() + + return SubTodosPublic(data=todos, count=count) + + +@router.get("/{id}", response_model=SubTodoPublic) +def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any: + """ + Get sub todo by ID. + """ + sub_todo = session.get(SubTodo, id) + if not sub_todo: + raise HTTPException(status_code=404, detail="Sub todo not found") + return sub_todo + +@router.post("/", response_model=SubTodoPublic) +def create_sub_todo( + *, session: SessionDep, sub_todo_in: SubTodoCreate +) -> Any: + """ + Create new sub todo. + """ + todo = session.get(Todo, sub_todo_in.todo_id) + if not todo: + raise HTTPException(status_code=404, detail="Todo not found") + sub_todo = SubTodoCreate.model_validate(sub_todo_in) + create_todo = SubTodo(**dict(sub_todo)) + session.add(create_todo) + session.commit() + session.refresh(create_todo) + return create_todo + + +# @router.put("/{id}", response_model=SubTodoPublic) +# def update_sub_todo( +# *, +# session: SessionDep, +# current_user: CurrentUser, +# id: uuid.UUID, +# todo_in: SubTodoUpdate, +# ) -> Any: +# """ +# Update an item. +# """ +# sub_todo = session.get(SubTodo, id) +# if not sub_todo: +# raise HTTPException(status_code=404, detail="SubTodo not found") +# if not current_user.is_superuser and (sub_todo.owner_id != current_user.id): +# raise HTTPException(status_code=400, detail="Not enough permissions") +# update_dict = todo_in.model_dump(exclude_unset=True) +# sub_todo.sqlmodel_update(update_dict) +# session.add(sub_todo) +# session.commit() +# session.refresh(sub_todo) +# return sub_todo + +@router.put("/{id}", response_model=SubTodoPublic) +def update_sub_todo( + *, + session: SessionDep, + current_user: CurrentUser, + id: uuid.UUID, + sub_todo_in: SubTodoUpdate, +) -> Any: + """ + Update a sub todo by ID. + """ + # Fetch the sub todo by ID + sub_todo = session.get(SubTodo, id) + if not sub_todo: + raise HTTPException(status_code=404, detail="SubTodo not found") + + # Fetch the associated parent todo for ownership verification + parent_todo = session.get(Todo, sub_todo.todo_id) + if not parent_todo: + raise HTTPException(status_code=404, detail="Parent Todo not found") + + # Permission check: Ensure the user is the owner or a superuser + if not current_user.is_superuser and (parent_todo.owner_id != current_user.id): + raise HTTPException(status_code=403, detail="Not enough permissions") + + # Update the SubTodo with the new data + update_data = sub_todo_in.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(sub_todo, key, value) + + # Commit changes to the database + session.add(sub_todo) + session.commit() + session.refresh(sub_todo) + + return sub_todo + +@router.delete("/{id}") +def delete_sub_todo( + session: SessionDep, current_user: CurrentUser, id: uuid.UUID +) -> Message: + """ + Delete a sub todo. + """ + sub_todo = session.get(SubTodo, id) + if not sub_todo: + raise HTTPException(status_code=404, detail="SubTodo not found") + todo = session.get(Todo, sub_todo.todo_id) + if not todo: + raise HTTPException(status_code=404, detail="Todo not found") + if not current_user.is_superuser and (todo.owner_id != current_user.id): + raise HTTPException(status_code=400, detail="Not enough permissions") + session.delete(sub_todo) + session.commit() + return Message(message="SubTodo deleted successfully") diff --git a/backend/app/crud.py b/backend/app/crud.py index 905bf48724..6759b54be0 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -4,7 +4,7 @@ from sqlmodel import Session, select from app.core.security import get_password_hash, verify_password -from app.models import Item, ItemCreate, User, UserCreate, UserUpdate +from app.models import Item, ItemCreate, User, UserCreate, UserUpdate, SubTodoCreate def create_user(*, session: Session, user_create: UserCreate) -> User: @@ -52,3 +52,10 @@ def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) - session.commit() session.refresh(db_item) return db_item + +def create_subtodo(*, session: Session, item_in: SubTodoCreate, owner_id: uuid.UUID) -> Item: + db_item = Item.model_validate(item_in, update={"owner_id": owner_id}) + session.add(db_item) + session.commit() + session.refresh(db_item) + return db_item diff --git a/backend/app/models.py b/backend/app/models.py index 19bd64289e..29319e85c8 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -131,7 +131,7 @@ class Todo(TodoBase, table=True): owner_id: uuid.UUID = Field( foreign_key="user.id", nullable=False, ondelete="CASCADE" ) - status: str = Field(max_length=255) + status: str = Field(default="in_progress", max_length=255) owner: User | None = Relationship(back_populates="todos") subtodos: list["SubTodo"] = Relationship(back_populates="todo") @@ -142,12 +142,12 @@ class TodoCreate(TodoBase): class TodoUpdate(TodoBase): title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore desc: str | None = Field(default=None, max_length=255) - status: str = Field(max_length=255) + status: str | None = Field(default=None, max_length=255) class TodoPublic(TodoBase): id: uuid.UUID owner_id: uuid.UUID - status: StatusEnum + status: str class TodosPublic(SQLModel): data: list[TodoPublic] @@ -158,27 +158,28 @@ class SubTodoBase(SQLModel): title: str = Field(min_length=1, max_length=255) desc: str = Field(max_length=255) + class SubTodo(SubTodoBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) todo_id: uuid.UUID = Field( foreign_key="todo.id", nullable=False, ondelete="CASCADE" ) - status: str = Field(max_length=255) + status: str = Field(default="in_progress", max_length=255) todo: Todo | None = Relationship(back_populates="subtodos") class SubTodoCreate(SubTodoBase): - pass + todo_id: uuid.UUID # Properties to receive on item update class SubTodoUpdate(SubTodoBase): title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore desc: str | None = Field(default=None, max_length=255) - status: StatusEnum | None = Field(default=None) + status: str | None = Field(default=None, max_length=255) class SubTodoPublic(SubTodoBase): id: uuid.UUID todo_id: uuid.UUID - status: StatusEnum + status: str class SubTodosPublic(SQLModel): data: list[SubTodoPublic] From 90c52d899a23c74a71b089049d97dfe3ef25e6a4 Mon Sep 17 00:00:00 2001 From: blueoc-ducphong Date: Mon, 9 Dec 2024 13:37:56 +0700 Subject: [PATCH 6/6] feat: Write api and design UI for To Do --- backend/app/api/main.py | 4 +- backend/app/api/routes/todos.py | 109 +++++++++++ backend/app/crud.py | 3 +- frontend/package-lock.json | 38 +++- frontend/package.json | 7 +- frontend/src/client/schemas.gen.ts | 74 ++++---- frontend/src/client/sdk.gen.ts | 84 ++++----- frontend/src/client/types.gen.ts | 42 +++-- .../src/components/Common/ActionsMenu.tsx | 24 ++- .../src/components/Common/DeleteAlert.tsx | 9 +- frontend/src/components/Common/Navbar.tsx | 4 +- .../src/components/Common/SidebarItems.tsx | 2 +- .../{Items/AddItem.tsx => todos/Addtodos.tsx} | 24 +-- .../EditItem.tsx => todos/Edittodos.tsx} | 45 +++-- frontend/src/components/ui/avatar.tsx | 74 ++++++++ frontend/src/components/ui/button.tsx | 40 ++++ frontend/src/components/ui/checkbox.tsx | 25 +++ frontend/src/components/ui/close-button.tsx | 17 ++ frontend/src/components/ui/color-mode.tsx | 67 +++++++ frontend/src/components/ui/dialog.tsx | 62 +++++++ frontend/src/components/ui/field.tsx | 33 ++++ frontend/src/components/ui/input-group.tsx | 50 +++++ frontend/src/components/ui/popover.tsx | 59 ++++++ frontend/src/components/ui/provider.tsx | 15 ++ frontend/src/components/ui/radio.tsx | 24 +++ frontend/src/components/ui/slider.tsx | 82 +++++++++ frontend/src/components/ui/tooltip.tsx | 46 +++++ frontend/src/routeTree.gen.ts | 20 +- frontend/src/routes/_layout/items.tsx | 135 -------------- frontend/src/routes/_layout/todos.tsx | 174 ++++++++++++++++++ 30 files changed, 1086 insertions(+), 306 deletions(-) create mode 100644 backend/app/api/routes/todos.py rename frontend/src/components/{Items/AddItem.tsx => todos/Addtodos.tsx} (80%) rename frontend/src/components/{Items/EditItem.tsx => todos/Edittodos.tsx} (70%) create mode 100644 frontend/src/components/ui/avatar.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/checkbox.tsx create mode 100644 frontend/src/components/ui/close-button.tsx create mode 100644 frontend/src/components/ui/color-mode.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/field.tsx create mode 100644 frontend/src/components/ui/input-group.tsx create mode 100644 frontend/src/components/ui/popover.tsx create mode 100644 frontend/src/components/ui/provider.tsx create mode 100644 frontend/src/components/ui/radio.tsx create mode 100644 frontend/src/components/ui/slider.tsx create mode 100644 frontend/src/components/ui/tooltip.tsx delete mode 100644 frontend/src/routes/_layout/items.tsx create mode 100644 frontend/src/routes/_layout/todos.tsx diff --git a/backend/app/api/main.py b/backend/app/api/main.py index eac18c8e8f..5ca76728f6 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.routes import items, login, private, users, utils +from app.api.routes import items, login, private, todos, users, utils from app.core.config import settings api_router = APIRouter() @@ -8,7 +8,7 @@ api_router.include_router(users.router) api_router.include_router(utils.router) api_router.include_router(items.router) - +api_router.include_router(todos.router) if settings.ENVIRONMENT == "local": api_router.include_router(private.router) diff --git a/backend/app/api/routes/todos.py b/backend/app/api/routes/todos.py new file mode 100644 index 0000000000..06d1ee35ed --- /dev/null +++ b/backend/app/api/routes/todos.py @@ -0,0 +1,109 @@ +import uuid +from typing import Any + +from fastapi import APIRouter, HTTPException +from sqlmodel import func, select + +from app.api.deps import CurrentUser, SessionDep +from app.models import Todo, TodoCreate, TodoPublic, TodosPublic, TodoUpdate, Message + +router = APIRouter(prefix="/todos", tags=["todos"]) + + +@router.get("/", response_model=TodosPublic) +def read_todos( + session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100 +) -> Any: + """ + Retrieve todos. + """ + + if current_user.is_superuser: + count_statement = select(func.count()).select_from(Todo) + count = session.exec(count_statement).one() + statement = select(Todo).offset(skip).limit(limit) + todos = session.exec(statement).all() + else: + count_statement = ( + select(func.count()) + .select_from(Todo) + .where(Todo.owner_id == current_user.id) + ) + count = session.exec(count_statement).one() + statement = ( + select(Todo) + .where(Todo.owner_id == current_user.id) + .offset(skip) + .limit(limit) + ) + todos = session.exec(statement).all() + + return TodosPublic(data=todos, count=count) + + +@router.get("/{id}", response_model=TodoPublic) +def read_todo(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any: + """ + Get todo by ID. + """ + todo = session.get(Todo, id) + if not todo: + raise HTTPException(status_code=404, detail="Task not found") + if not current_user.is_superuser and (todo.owner_id != current_user.id): + raise HTTPException(status_code=400, detail="Not enough permissions") + return todo + +# CurrentUser to authenticat ->middleware +@router.post("/", response_model=TodoPublic) +def create_todo( + *, session: SessionDep, current_user: CurrentUser, todo_in: TodoCreate +) -> TodoPublic: + """ + Create new todo. + """ + todo = Todo.model_validate(todo_in, update={"owner_id": current_user.id}) + session.add(todo) + session.commit() + session.refresh(todo) + return todo + + +@router.put("/{id}", response_model=TodoPublic) +def update_todo( + *, + session: SessionDep, + current_user: CurrentUser, + id: uuid.UUID, + todo_in: TodoUpdate, +) -> Any: + """ + Update an item. + """ + todo = session.get(Todo, id) + if not todo: + raise HTTPException(status_code=404, detail="Task not found") + if not current_user.is_superuser and (todo.owner_id != current_user.id): + raise HTTPException(status_code=400, detail="Not enough permissions") + update_dict = todo_in.model_dump(exclude_unset=True) + todo.sqlmodel_update(update_dict) + session.add(todo) + session.commit() + session.refresh(todo) + return todo + + +@router.delete("/{id}") +def delete_item( + session: SessionDep, current_user: CurrentUser, id: uuid.UUID +) -> Message: + """ + Delete a todo. + """ + todo = session.get(Todo, id) + if not todo: + raise HTTPException(status_code=404, detail="Task not found") + if not current_user.is_superuser and (todo.owner_id != current_user.id): + raise HTTPException(status_code=400, detail="Not enough permissions") + session.delete(todo) + session.commit() + return Message(message="Task deleted successfully") diff --git a/backend/app/crud.py b/backend/app/crud.py index 905bf48724..8355299349 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -4,7 +4,7 @@ from sqlmodel import Session, select from app.core.security import get_password_hash, verify_password -from app.models import Item, ItemCreate, User, UserCreate, UserUpdate +from app.models import Item, ItemCreate, Todo, TodoCreate, User, UserCreate, UserUpdate def create_user(*, session: Session, user_create: UserCreate) -> User: @@ -52,3 +52,4 @@ def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) - session.commit() session.refresh(db_item) return db_item + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b1d42d6704..4ef67a7a22 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.0", "dependencies": { "@chakra-ui/icons": "2.1.1", - "@chakra-ui/react": "2.8.2", - "@emotion/react": "11.11.3", + "@chakra-ui/react": "^2.8.2", + "@emotion/react": "^11.11.3", "@emotion/styled": "11.11.0", "@tanstack/react-query": "^5.28.14", "@tanstack/react-query-devtools": "^5.28.14", @@ -18,11 +18,12 @@ "axios": "1.7.4", "form-data": "4.0.0", "framer-motion": "10.16.16", + "next-themes": "^0.4.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", "react-hook-form": "7.49.3", - "react-icons": "5.0.1" + "react-icons": "^5.4.0" }, "devDependencies": { "@biomejs/biome": "1.6.1", @@ -988,6 +989,7 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-2.8.2.tgz", "integrity": "sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==", + "license": "MIT", "dependencies": { "@chakra-ui/accordion": "2.3.1", "@chakra-ui/alert": "2.2.2", @@ -1648,6 +1650,7 @@ "version": "11.11.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", @@ -3698,6 +3701,16 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/next-themes": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz", + "integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-fetch-native": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", @@ -4071,9 +4084,10 @@ } }, "node_modules/react-icons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", - "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "license": "MIT", "peerDependencies": { "react": "*" } @@ -7025,6 +7039,12 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "next-themes": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz", + "integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==", + "requires": {} + }, "node-fetch-native": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", @@ -7271,9 +7291,9 @@ "requires": {} }, "react-icons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", - "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", "requires": {} }, "react-is": { diff --git a/frontend/package.json b/frontend/package.json index 546028c9a9..8ba1b99d31 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,8 +12,8 @@ }, "dependencies": { "@chakra-ui/icons": "2.1.1", - "@chakra-ui/react": "2.8.2", - "@emotion/react": "11.11.3", + "@chakra-ui/react": "^2.8.2", + "@emotion/react": "^11.11.3", "@emotion/styled": "11.11.0", "@tanstack/react-query": "^5.28.14", "@tanstack/react-query-devtools": "^5.28.14", @@ -21,11 +21,12 @@ "axios": "1.7.4", "form-data": "4.0.0", "framer-motion": "10.16.16", + "next-themes": "^0.4.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", "react-hook-form": "7.49.3", - "react-icons": "5.0.1" + "react-icons": "^5.4.0" }, "devDependencies": { "@biomejs/biome": "1.6.1", diff --git a/frontend/src/client/schemas.gen.ts b/frontend/src/client/schemas.gen.ts index ca22051056..35d39daa4a 100644 --- a/frontend/src/client/schemas.gen.ts +++ b/frontend/src/client/schemas.gen.ts @@ -69,7 +69,7 @@ export const HTTPValidationErrorSchema = { title: "HTTPValidationError", } as const -export const ItemCreateSchema = { +export const TodoCreateSchema = { properties: { title: { type: "string", @@ -77,25 +77,24 @@ export const ItemCreateSchema = { minLength: 1, title: "Title", }, - description: { - anyOf: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], + desc: { + type: ["string", "null"], // Sửa lại từ description => desc để đồng bộ backend + maxLength: 255, title: "Description", }, + status: { + type: "string", + enum: ["pending", "completed", "in_progress"], // Thêm trạng thái status + default: "in_progress", + title: "Status", + }, }, type: "object", required: ["title"], - title: "ItemCreate", + title: "TodoCreate", } as const -export const ItemPublicSchema = { +export const TodoPublicSchema = { properties: { title: { type: "string", @@ -103,16 +102,9 @@ export const ItemPublicSchema = { minLength: 1, title: "Title", }, - description: { - anyOf: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], + desc: { + type: ["string", "null"], + maxLength: 255, title: "Description", }, id: { @@ -125,13 +117,19 @@ export const ItemPublicSchema = { format: "uuid", title: "Owner Id", }, + status: { + type: "string", + enum: ["pending", "completed", "in_progress"], + default: "in_progress", + title: "Status", + }, }, type: "object", required: ["title", "id", "owner_id"], - title: "ItemPublic", + title: "TodoPublic", } as const -export const ItemUpdateSchema = { +export const TodoUpdateSchema = { properties: { title: { anyOf: [ @@ -146,28 +144,26 @@ export const ItemUpdateSchema = { ], title: "Title", }, - description: { - anyOf: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], + desc: { + type: ["string", "null"], + maxLength: 255, title: "Description", }, + status: { + type: ["string", "null"], + enum: ["pending", "completed", "in_progress"], + title: "Status", + }, }, type: "object", - title: "ItemUpdate", + title: "TodoUpdate", } as const -export const ItemsPublicSchema = { +export const TodosPublicSchema = { properties: { data: { items: { - $ref: "#/components/schemas/ItemPublic", + $ref: "#/components/schemas/TodoPublic", }, type: "array", title: "Data", @@ -179,7 +175,7 @@ export const ItemsPublicSchema = { }, type: "object", required: ["data", "count"], - title: "ItemsPublic", + title: "TodosPublic", } as const export const MessageSchema = { diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts index 92ded2bde8..6f4d45d16c 100644 --- a/frontend/src/client/sdk.gen.ts +++ b/frontend/src/client/sdk.gen.ts @@ -4,16 +4,16 @@ import type { CancelablePromise } from "./core/CancelablePromise" import { OpenAPI } from "./core/OpenAPI" import { request as __request } from "./core/request" import type { - ItemsReadItemsData, - ItemsReadItemsResponse, - ItemsCreateItemData, - ItemsCreateItemResponse, - ItemsReadItemData, - ItemsReadItemResponse, - ItemsUpdateItemData, - ItemsUpdateItemResponse, - ItemsDeleteItemData, - ItemsDeleteItemResponse, + TodosReadTodosData, + TodosReadTodosResponse, + TodosCreateTodoData, + TodosCreateTodoResponse, + TodosReadTodoData, + TodosReadTodoResponse, + TodosUpdateTodoData, + TodosUpdateTodoResponse, + TodosDeleteTodoData, + TodosDeleteTodoResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, @@ -46,9 +46,9 @@ import type { UtilsHealthCheckResponse, } from "./types.gen" -export class ItemsService { +export class TodosService { /** - * Read Items + * Read Todos * Retrieve items. * @param data The data for the request. * @param data.skip @@ -56,12 +56,12 @@ export class ItemsService { * @returns ItemsPublic Successful Response * @throws ApiError */ - public static readItems( - data: ItemsReadItemsData = {}, - ): CancelablePromise { + public static readTodos( + data: TodosReadTodosData = {}, + ): CancelablePromise { return __request(OpenAPI, { method: "GET", - url: "/api/v1/items/", + url: "/api/v1/todos/", query: { skip: data.skip, limit: data.limit, @@ -73,19 +73,19 @@ export class ItemsService { } /** - * Create Item - * Create new item. + * Create Todo + * Create new todo. * @param data The data for the request. * @param data.requestBody - * @returns ItemPublic Successful Response + * @returns TodoPublic Successful Response * @throws ApiError */ - public static createItem( - data: ItemsCreateItemData, - ): CancelablePromise { + public static createTodo( + data: TodosCreateTodoData, + ): CancelablePromise { return __request(OpenAPI, { method: "POST", - url: "/api/v1/items/", + url: "/api/v1/todos/", body: data.requestBody, mediaType: "application/json", errors: { @@ -95,19 +95,19 @@ export class ItemsService { } /** - * Read Item - * Get item by ID. + * Read Todo + * Get todo by ID. * @param data The data for the request. * @param data.id - * @returns ItemPublic Successful Response + * @returns TodoPublic Successful Response * @throws ApiError */ public static readItem( - data: ItemsReadItemData, - ): CancelablePromise { + data: TodosReadTodoData, + ): CancelablePromise { return __request(OpenAPI, { method: "GET", - url: "/api/v1/items/{id}", + url: "/api/v1/todos/{id}", path: { id: data.id, }, @@ -118,20 +118,20 @@ export class ItemsService { } /** - * Update Item - * Update an item. + * Update Todo + * Update an todo. * @param data The data for the request. * @param data.id * @param data.requestBody - * @returns ItemPublic Successful Response + * @returns TodoPublic Successful Response * @throws ApiError */ - public static updateItem( - data: ItemsUpdateItemData, - ): CancelablePromise { + public static updateTodo( + data: TodosUpdateTodoData, + ): CancelablePromise { return __request(OpenAPI, { method: "PUT", - url: "/api/v1/items/{id}", + url: "/api/v1/todos/{id}", path: { id: data.id, }, @@ -144,19 +144,19 @@ export class ItemsService { } /** - * Delete Item - * Delete an item. + * Delete Todo + * Delete an todo. * @param data The data for the request. * @param data.id * @returns Message Successful Response * @throws ApiError */ - public static deleteItem( - data: ItemsDeleteItemData, - ): CancelablePromise { + public static deleteTodo( + data: TodosDeleteTodoData, + ): CancelablePromise { return __request(OpenAPI, { method: "DELETE", - url: "/api/v1/items/{id}", + url: "/api/v1/todos/{id}", path: { id: data.id, }, diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index c2a58d06cb..9059513b54 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -13,26 +13,28 @@ export type HTTPValidationError = { detail?: Array } -export type ItemCreate = { +export type TodoCreate = { title: string - description?: string | null + desc?: string | null } -export type ItemPublic = { +export type TodoPublic = { title: string - description?: string | null + desc?: string | null id: string owner_id: string + status: 'pending' | 'completed' | 'in_progress' } -export type ItemsPublic = { - data: Array +export type TodosPublic = { + data: Array count: number } -export type ItemUpdate = { +export type TodoUpdate = { title?: string | null - description?: string | null + desc?: string | null + status?: 'pending' | 'completed' | 'in_progress' | null } export type Message = { @@ -100,37 +102,37 @@ export type ValidationError = { type: string } -export type ItemsReadItemsData = { +export type TodosReadTodosData = { limit?: number skip?: number } -export type ItemsReadItemsResponse = ItemsPublic +export type TodosReadTodosResponse = TodosPublic -export type ItemsCreateItemData = { - requestBody: ItemCreate +export type TodosCreateTodoData = { + requestBody: TodoCreate } -export type ItemsCreateItemResponse = ItemPublic +export type TodosCreateTodoResponse = TodoPublic -export type ItemsReadItemData = { +export type TodosReadTodoData = { id: string } -export type ItemsReadItemResponse = ItemPublic +export type TodosReadTodoResponse = TodoPublic -export type ItemsUpdateItemData = { +export type TodosUpdateTodoData = { id: string - requestBody: ItemUpdate + requestBody: TodoUpdate } -export type ItemsUpdateItemResponse = ItemPublic +export type TodosUpdateTodoResponse = TodoPublic -export type ItemsDeleteItemData = { +export type TodosDeleteTodoData = { id: string } -export type ItemsDeleteItemResponse = Message +export type TodosDeleteTodoResponse = Message export type LoginLoginAccessTokenData = { formData: Body_login_login_access_token diff --git a/frontend/src/components/Common/ActionsMenu.tsx b/frontend/src/components/Common/ActionsMenu.tsx index 4ff94ee3ea..a2607d71f9 100644 --- a/frontend/src/components/Common/ActionsMenu.tsx +++ b/frontend/src/components/Common/ActionsMenu.tsx @@ -6,23 +6,27 @@ import { MenuList, useDisclosure, } from "@chakra-ui/react" -import { BsThreeDotsVertical } from "react-icons/bs" -import { FiEdit, FiTrash } from "react-icons/fi" +import { BsThreeDotsVertical } from "react-icons/bs" +import {FiEdit, FiPlus, FiTrash } from "react-icons/fi" -import type { ItemPublic, UserPublic } from "../../client" +import type { TodoPublic, UserPublic } from "../../client" import EditUser from "../Admin/EditUser" -import EditItem from "../Items/EditItem" +import Edittodos from "../todos/Edittodos" import Delete from "./DeleteAlert" interface ActionsMenuProps { type: string - value: ItemPublic | UserPublic + value: TodoPublic | UserPublic disabled?: boolean } const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => { const editUserModal = useDisclosure() const deleteModal = useDisclosure() + const addSubtask = () => { + console.log("add subtask") + } + return ( <> @@ -40,6 +44,12 @@ const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => { > Edit {type} + } + > + Add Subtask + } @@ -55,8 +65,8 @@ const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => { onClose={editUserModal.onClose} /> ) : ( - diff --git a/frontend/src/components/Common/DeleteAlert.tsx b/frontend/src/components/Common/DeleteAlert.tsx index 1528fc5fe1..9ed0c72b22 100644 --- a/frontend/src/components/Common/DeleteAlert.tsx +++ b/frontend/src/components/Common/DeleteAlert.tsx @@ -10,8 +10,7 @@ import { import { useMutation, useQueryClient } from "@tanstack/react-query" import React from "react" import { useForm } from "react-hook-form" - -import { ItemsService, UsersService } from "../../client" +import { TodosService, UsersService } from "../../client" import useCustomToast from "../../hooks/useCustomToast" interface DeleteProps { @@ -31,8 +30,8 @@ const Delete = ({ type, id, isOpen, onClose }: DeleteProps) => { } = useForm() const deleteEntity = async (id: string) => { - if (type === "Item") { - await ItemsService.deleteItem({ id: id }) + if (type === "Todo") { + await TodosService.deleteTodo({ id: id }) } else if (type === "User") { await UsersService.deleteUser({ userId: id }) } else { @@ -59,7 +58,7 @@ const Delete = ({ type, id, isOpen, onClose }: DeleteProps) => { }, onSettled: () => { queryClient.invalidateQueries({ - queryKey: [type === "Item" ? "items" : "users"], + queryKey: [type === "Todo" ? "todos" : "users"], }) }, }) diff --git a/frontend/src/components/Common/Navbar.tsx b/frontend/src/components/Common/Navbar.tsx index 2aba31c362..cc505ae30e 100644 --- a/frontend/src/components/Common/Navbar.tsx +++ b/frontend/src/components/Common/Navbar.tsx @@ -8,7 +8,7 @@ interface NavbarProps { addModalAs: ComponentType | ElementType } -const Navbar = ({ type, addModalAs }: NavbarProps) => { +const Navbar = ({ addModalAs }: NavbarProps) => { const addModal = useDisclosure() const AddModal = addModalAs @@ -28,7 +28,7 @@ const Navbar = ({ type, addModalAs }: NavbarProps) => { fontSize={{ base: "sm", md: "inherit" }} onClick={addModal.onOpen} > - Add {type} + Add Task diff --git a/frontend/src/components/Common/SidebarItems.tsx b/frontend/src/components/Common/SidebarItems.tsx index 929e8f785e..419a23f20f 100644 --- a/frontend/src/components/Common/SidebarItems.tsx +++ b/frontend/src/components/Common/SidebarItems.tsx @@ -7,7 +7,7 @@ import type { UserPublic } from "../../client" const items = [ { icon: FiHome, title: "Dashboard", path: "/" }, - { icon: FiBriefcase, title: "Items", path: "/items" }, + { icon: FiBriefcase, title: "Task", path: "/todos" }, { icon: FiSettings, title: "User Settings", path: "/settings" }, ] diff --git a/frontend/src/components/Items/AddItem.tsx b/frontend/src/components/todos/Addtodos.tsx similarity index 80% rename from frontend/src/components/Items/AddItem.tsx rename to frontend/src/components/todos/Addtodos.tsx index fa5682da3f..149ee9bb80 100644 --- a/frontend/src/components/Items/AddItem.tsx +++ b/frontend/src/components/todos/Addtodos.tsx @@ -15,7 +15,7 @@ import { import { useMutation, useQueryClient } from "@tanstack/react-query" import { type SubmitHandler, useForm } from "react-hook-form" -import { type ApiError, type ItemCreate, ItemsService } from "../../client" +import { type ApiError, type TodoCreate, TodosService } from "../../client" import useCustomToast from "../../hooks/useCustomToast" import { handleError } from "../../utils" @@ -32,20 +32,20 @@ const AddItem = ({ isOpen, onClose }: AddItemProps) => { handleSubmit, reset, formState: { errors, isSubmitting }, - } = useForm({ + } = useForm({ mode: "onBlur", criteriaMode: "all", defaultValues: { title: "", - description: "", + desc: "", }, }) const mutation = useMutation({ - mutationFn: (data: ItemCreate) => - ItemsService.createItem({ requestBody: data }), + mutationFn: (data: TodoCreate) => + TodosService.createTodo({ requestBody: data }), onSuccess: () => { - showToast("Success!", "Item created successfully.", "success") + showToast("Success!", "Task created successfully.", "success") reset() onClose() }, @@ -53,11 +53,11 @@ const AddItem = ({ isOpen, onClose }: AddItemProps) => { handleError(err, showToast) }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["items"] }) + queryClient.invalidateQueries({ queryKey: ["todos"] }) }, }) - const onSubmit: SubmitHandler = (data) => { + const onSubmit: SubmitHandler = (data) => { mutation.mutate(data) } @@ -71,7 +71,7 @@ const AddItem = ({ isOpen, onClose }: AddItemProps) => { > - Add Item + Add Task @@ -89,10 +89,10 @@ const AddItem = ({ isOpen, onClose }: AddItemProps) => { )} - Description + Description diff --git a/frontend/src/components/Items/EditItem.tsx b/frontend/src/components/todos/Edittodos.tsx similarity index 70% rename from frontend/src/components/Items/EditItem.tsx rename to frontend/src/components/todos/Edittodos.tsx index 3d40cdc03a..07e442b80e 100644 --- a/frontend/src/components/Items/EditItem.tsx +++ b/frontend/src/components/todos/Edittodos.tsx @@ -17,20 +17,20 @@ import { type SubmitHandler, useForm } from "react-hook-form" import { type ApiError, - type ItemPublic, - type ItemUpdate, - ItemsService, + type TodoPublic, + type TodoUpdate, + TodosService, } from "../../client" import useCustomToast from "../../hooks/useCustomToast" import { handleError } from "../../utils" -interface EditItemProps { - item: ItemPublic +interface EditTodoProps { + todo: TodoPublic isOpen: boolean onClose: () => void } -const EditItem = ({ item, isOpen, onClose }: EditItemProps) => { +const Edittodos = ({ todo, isOpen, onClose }: EditTodoProps) => { const queryClient = useQueryClient() const showToast = useCustomToast() const { @@ -38,28 +38,37 @@ const EditItem = ({ item, isOpen, onClose }: EditItemProps) => { handleSubmit, reset, formState: { isSubmitting, errors, isDirty }, - } = useForm({ + } = useForm({ mode: "onBlur", criteriaMode: "all", - defaultValues: item, + defaultValues: todo, }) const mutation = useMutation({ - mutationFn: (data: ItemUpdate) => - ItemsService.updateItem({ id: item.id, requestBody: data }), + mutationFn: (data: TodoUpdate) => + TodosService.updateTodo({ id: todo.id, requestBody: data }), onSuccess: () => { - showToast("Success!", "Item updated successfully.", "success") + showToast("Success!", "Task updated successfully.", "success") onClose() }, onError: (err: ApiError) => { handleError(err, showToast) }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["items"] }) + queryClient.setQueryData(["todos"], (oldData: any) => { + if (!oldData) return oldData; + return { + ...oldData, + data: oldData.data.map((todo: any) => + todo.id === todo.id ? { ...todo } : todo + ), + }; + }); + queryClient.invalidateQueries({ queryKey: ["todos"] }) }, }) - const onSubmit: SubmitHandler = async (data) => { + const onSubmit: SubmitHandler = async (data) => { mutation.mutate(data) } @@ -78,7 +87,7 @@ const EditItem = ({ item, isOpen, onClose }: EditItemProps) => { > - Edit Item + Edit Task @@ -95,10 +104,10 @@ const EditItem = ({ item, isOpen, onClose }: EditItemProps) => { )} - Description + Description @@ -121,4 +130,4 @@ const EditItem = ({ item, isOpen, onClose }: EditItemProps) => { ) } -export default EditItem +export default Edittodos diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000000..cd84664ae9 --- /dev/null +++ b/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,74 @@ +"use client" + +import type { GroupProps, SlotRecipeProps } from "@chakra-ui/react" +import { Avatar as ChakraAvatar, Group } from "@chakra-ui/react" +import * as React from "react" + +type ImageProps = React.ImgHTMLAttributes + +export interface AvatarProps extends ChakraAvatar.RootProps { + name?: string + src?: string + srcSet?: string + loading?: ImageProps["loading"] + icon?: React.ReactElement + fallback?: React.ReactNode +} + +export const Avatar = React.forwardRef( + function Avatar(props, ref) { + const { name, src, srcSet, loading, icon, fallback, children, ...rest } = + props + return ( + + + {fallback} + + + {children} + + ) + }, +) + +interface AvatarFallbackProps extends ChakraAvatar.FallbackProps { + name?: string + icon?: React.ReactElement +} + +const AvatarFallback = React.forwardRef( + function AvatarFallback(props, ref) { + const { name, icon, children, ...rest } = props + return ( + + {children} + {name != null && children == null && <>{getInitials(name)}} + {name == null && children == null && ( + {icon} + )} + + ) + }, +) + +function getInitials(name: string) { + const names = name.trim().split(" ") + const firstName = names[0] != null ? names[0] : "" + const lastName = names.length > 1 ? names[names.length - 1] : "" + return firstName && lastName + ? `${firstName.charAt(0)}${lastName.charAt(0)}` + : firstName.charAt(0) +} + +interface AvatarGroupProps extends GroupProps, SlotRecipeProps<"avatar"> {} + +export const AvatarGroup = React.forwardRef( + function AvatarGroup(props, ref) { + const { size, variant, borderless, ...rest } = props + return ( + + + + ) + }, +) diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000000..815e42456e --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,40 @@ +import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react" +import { + AbsoluteCenter, + Button as ChakraButton, + Text, + Spinner, +} from "@chakra-ui/react" +import * as React from "react" + +interface ButtonLoadingProps { + loading?: boolean + loadingText?: React.ReactNode +} + +export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {} + +export const Button = React.forwardRef( + function Button(props, ref) { + const { loading, disabled, loadingText, children, ...rest } = props + return ( + + {loading && !loadingText ? ( + <> + + + + {children} + + ) : loading && loadingText ? ( + <> + + {loadingText} + + ) : ( + children + )} + + ) + }, +) diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000000..2a27c2ffb3 --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,25 @@ +import { Checkbox as ChakraCheckbox } from "@chakra-ui/react" +import * as React from "react" + +export interface CheckboxProps extends ChakraCheckbox.RootProps { + icon?: React.ReactNode + inputProps?: React.InputHTMLAttributes + rootRef?: React.Ref +} + +export const Checkbox = React.forwardRef( + function Checkbox(props, ref) { + const { icon, children, inputProps, rootRef, ...rest } = props + return ( + + + + {icon || } + + {children != null && ( + {children} + )} + + ) + }, +) diff --git a/frontend/src/components/ui/close-button.tsx b/frontend/src/components/ui/close-button.tsx new file mode 100644 index 0000000000..94af488598 --- /dev/null +++ b/frontend/src/components/ui/close-button.tsx @@ -0,0 +1,17 @@ +import type { ButtonProps } from "@chakra-ui/react" +import { IconButton as ChakraIconButton } from "@chakra-ui/react" +import * as React from "react" +import { LuX } from "react-icons/lu" + +export type CloseButtonProps = ButtonProps + +export const CloseButton = React.forwardRef< + HTMLButtonElement, + CloseButtonProps +>(function CloseButton(props, ref) { + return ( + + {props.children ?? } + + ) +}) diff --git a/frontend/src/components/ui/color-mode.tsx b/frontend/src/components/ui/color-mode.tsx new file mode 100644 index 0000000000..a34b9689c2 --- /dev/null +++ b/frontend/src/components/ui/color-mode.tsx @@ -0,0 +1,67 @@ +"use client" + +import type { IconButtonProps } from "@chakra-ui/react" +import { ClientOnly, IconButton, Skeleton } from "@chakra-ui/react" +import { ThemeProvider, useTheme } from "next-themes" +import type { ThemeProviderProps } from "next-themes" +import * as React from "react" +import { LuMoon, LuSun } from "react-icons/lu" + +export interface ColorModeProviderProps extends ThemeProviderProps {} + +export function ColorModeProvider(props: ColorModeProviderProps) { + return ( + + ) +} + +export function useColorMode() { + const { resolvedTheme, setTheme } = useTheme() + const toggleColorMode = () => { + setTheme(resolvedTheme === "light" ? "dark" : "light") + } + return { + colorMode: resolvedTheme, + setColorMode: setTheme, + toggleColorMode, + } +} + +export function useColorModeValue(light: T, dark: T) { + const { colorMode } = useColorMode() + return colorMode === "light" ? light : dark +} + +export function ColorModeIcon() { + const { colorMode } = useColorMode() + return colorMode === "light" ? : +} + +interface ColorModeButtonProps extends Omit {} + +export const ColorModeButton = React.forwardRef< + HTMLButtonElement, + ColorModeButtonProps +>(function ColorModeButton(props, ref) { + const { toggleColorMode } = useColorMode() + return ( + }> + + + + + ) +}) diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000000..89d68a5def --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,62 @@ +import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react" +import { CloseButton } from "./close-button" +import * as React from "react" + +interface DialogContentProps extends ChakraDialog.ContentProps { + portalled?: boolean + portalRef?: React.RefObject + backdrop?: boolean +} + +export const DialogContent = React.forwardRef< + HTMLDivElement, + DialogContentProps +>(function DialogContent(props, ref) { + const { + children, + portalled = true, + portalRef, + backdrop = true, + ...rest + } = props + + return ( + + {backdrop && } + + + {children} + + + + ) +}) + +export const DialogCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDialog.CloseTriggerProps +>(function DialogCloseTrigger(props, ref) { + return ( + + + {props.children} + + + ) +}) + +export const DialogRoot = ChakraDialog.Root +export const DialogFooter = ChakraDialog.Footer +export const DialogHeader = ChakraDialog.Header +export const DialogBody = ChakraDialog.Body +export const DialogBackdrop = ChakraDialog.Backdrop +export const DialogTitle = ChakraDialog.Title +export const DialogDescription = ChakraDialog.Description +export const DialogTrigger = ChakraDialog.Trigger +export const DialogActionTrigger = ChakraDialog.ActionTrigger diff --git a/frontend/src/components/ui/field.tsx b/frontend/src/components/ui/field.tsx new file mode 100644 index 0000000000..dd3b66f100 --- /dev/null +++ b/frontend/src/components/ui/field.tsx @@ -0,0 +1,33 @@ +import { Field as ChakraField } from "@chakra-ui/react" +import * as React from "react" + +export interface FieldProps extends Omit { + label?: React.ReactNode + helperText?: React.ReactNode + errorText?: React.ReactNode + optionalText?: React.ReactNode +} + +export const Field = React.forwardRef( + function Field(props, ref) { + const { label, children, helperText, errorText, optionalText, ...rest } = + props + return ( + + {label && ( + + {label} + + + )} + {children} + {helperText && ( + {helperText} + )} + {errorText && ( + {errorText} + )} + + ) + }, +) diff --git a/frontend/src/components/ui/input-group.tsx b/frontend/src/components/ui/input-group.tsx new file mode 100644 index 0000000000..1124a61a21 --- /dev/null +++ b/frontend/src/components/ui/input-group.tsx @@ -0,0 +1,50 @@ +import type { BoxProps, InputElementProps } from "@chakra-ui/react" +import { Group, InputElement } from "@chakra-ui/react" +import * as React from "react" + +export interface InputGroupProps extends BoxProps { + startElementProps?: InputElementProps + endElementProps?: InputElementProps + startElement?: React.ReactNode + endElement?: React.ReactNode + children: React.ReactElement + startOffset?: InputElementProps["paddingStart"] + endOffset?: InputElementProps["paddingEnd"] +} + +export const InputGroup = React.forwardRef( + function InputGroup(props, ref) { + const { + startElement, + startElementProps, + endElement, + endElementProps, + children, + startOffset = "6px", + endOffset = "6px", + ...rest + } = props + + return ( + + {startElement && ( + + {startElement} + + )} + {React.cloneElement(children, { + ...(startElement && { + ps: `calc(var(--input-height) - ${startOffset})`, + }), + ...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }), + ...children.props, + })} + {endElement && ( + + {endElement} + + )} + + ) + }, +) diff --git a/frontend/src/components/ui/popover.tsx b/frontend/src/components/ui/popover.tsx new file mode 100644 index 0000000000..3320659db7 --- /dev/null +++ b/frontend/src/components/ui/popover.tsx @@ -0,0 +1,59 @@ +import { Popover as ChakraPopover, Portal } from "@chakra-ui/react" +import { CloseButton } from "./close-button" +import * as React from "react" + +interface PopoverContentProps extends ChakraPopover.ContentProps { + portalled?: boolean + portalRef?: React.RefObject +} + +export const PopoverContent = React.forwardRef< + HTMLDivElement, + PopoverContentProps +>(function PopoverContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props + return ( + + + + + + ) +}) + +export const PopoverArrow = React.forwardRef< + HTMLDivElement, + ChakraPopover.ArrowProps +>(function PopoverArrow(props, ref) { + return ( + + + + ) +}) + +export const PopoverCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPopover.CloseTriggerProps +>(function PopoverCloseTrigger(props, ref) { + return ( + + + + ) +}) + +export const PopoverTitle = ChakraPopover.Title +export const PopoverDescription = ChakraPopover.Description +export const PopoverFooter = ChakraPopover.Footer +export const PopoverHeader = ChakraPopover.Header +export const PopoverRoot = ChakraPopover.Root +export const PopoverBody = ChakraPopover.Body +export const PopoverTrigger = ChakraPopover.Trigger diff --git a/frontend/src/components/ui/provider.tsx b/frontend/src/components/ui/provider.tsx new file mode 100644 index 0000000000..fd0331bf99 --- /dev/null +++ b/frontend/src/components/ui/provider.tsx @@ -0,0 +1,15 @@ +"use client" + +import { ChakraProvider, defaultSystem } from "@chakra-ui/react" +import { + ColorModeProvider, + type ColorModeProviderProps, +} from "./color-mode" + +export function Provider(props: ColorModeProviderProps) { + return ( + + + + ) +} diff --git a/frontend/src/components/ui/radio.tsx b/frontend/src/components/ui/radio.tsx new file mode 100644 index 0000000000..b3919d08c8 --- /dev/null +++ b/frontend/src/components/ui/radio.tsx @@ -0,0 +1,24 @@ +import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react" +import * as React from "react" + +export interface RadioProps extends ChakraRadioGroup.ItemProps { + rootRef?: React.Ref + inputProps?: React.InputHTMLAttributes +} + +export const Radio = React.forwardRef( + function Radio(props, ref) { + const { children, inputProps, rootRef, ...rest } = props + return ( + + + + {children && ( + {children} + )} + + ) + }, +) + +export const RadioGroup = ChakraRadioGroup.Root diff --git a/frontend/src/components/ui/slider.tsx b/frontend/src/components/ui/slider.tsx new file mode 100644 index 0000000000..55a7283bc0 --- /dev/null +++ b/frontend/src/components/ui/slider.tsx @@ -0,0 +1,82 @@ +import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react" +import * as React from "react" + +export interface SliderProps extends ChakraSlider.RootProps { + marks?: Array + label?: React.ReactNode + showValue?: boolean +} + +export const Slider = React.forwardRef( + function Slider(props, ref) { + const { marks: marksProp, label, showValue, ...rest } = props + const value = props.defaultValue ?? props.value + + const marks = marksProp?.map((mark) => { + if (typeof mark === "number") return { value: mark, label: undefined } + return mark + }) + + const hasMarkLabel = !!marks?.some((mark) => mark.label) + + return ( + + {label && !showValue && ( + {label} + )} + {label && showValue && ( + + {label} + + + )} + + + + + + + + + ) + }, +) + +function SliderThumbs(props: { value?: number[] }) { + const { value } = props + return ( + + {(_, index) => ( + + + + )} + + ) +} + +interface SliderMarksProps { + marks?: Array +} + +const SliderMarks = React.forwardRef( + function SliderMarks(props, ref) { + const { marks } = props + if (!marks?.length) return null + + return ( + + {marks.map((mark, index) => { + const value = typeof mark === "number" ? mark : mark.value + const label = typeof mark === "number" ? undefined : mark.label + return ( + + + {label} + + ) + })} + + ) + }, +) diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx new file mode 100644 index 0000000000..644c37cee4 --- /dev/null +++ b/frontend/src/components/ui/tooltip.tsx @@ -0,0 +1,46 @@ +import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react" +import * as React from "react" + +export interface TooltipProps extends ChakraTooltip.RootProps { + showArrow?: boolean + portalled?: boolean + portalRef?: React.RefObject + content: React.ReactNode + contentProps?: ChakraTooltip.ContentProps + disabled?: boolean +} + +export const Tooltip = React.forwardRef( + function Tooltip(props, ref) { + const { + showArrow, + children, + disabled, + portalled, + content, + contentProps, + portalRef, + ...rest + } = props + + if (disabled) return children + + return ( + + {children} + + + + {showArrow && ( + + + + )} + {content} + + + + + ) + }, +) diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 0e78c9ba20..4fd74624e3 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -17,8 +17,8 @@ import { Route as RecoverPasswordImport } from './routes/recover-password' import { Route as LoginImport } from './routes/login' import { Route as LayoutImport } from './routes/_layout' import { Route as LayoutIndexImport } from './routes/_layout/index' +import { Route as LayoutTodosImport } from './routes/_layout/todos' import { Route as LayoutSettingsImport } from './routes/_layout/settings' -import { Route as LayoutItemsImport } from './routes/_layout/items' import { Route as LayoutAdminImport } from './routes/_layout/admin' // Create/Update Routes @@ -53,13 +53,13 @@ const LayoutIndexRoute = LayoutIndexImport.update({ getParentRoute: () => LayoutRoute, } as any) -const LayoutSettingsRoute = LayoutSettingsImport.update({ - path: '/settings', +const LayoutTodosRoute = LayoutTodosImport.update({ + path: '/todos', getParentRoute: () => LayoutRoute, } as any) -const LayoutItemsRoute = LayoutItemsImport.update({ - path: '/items', +const LayoutSettingsRoute = LayoutSettingsImport.update({ + path: '/settings', getParentRoute: () => LayoutRoute, } as any) @@ -96,14 +96,14 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAdminImport parentRoute: typeof LayoutImport } - '/_layout/items': { - preLoaderRoute: typeof LayoutItemsImport - parentRoute: typeof LayoutImport - } '/_layout/settings': { preLoaderRoute: typeof LayoutSettingsImport parentRoute: typeof LayoutImport } + '/_layout/todos': { + preLoaderRoute: typeof LayoutTodosImport + parentRoute: typeof LayoutImport + } '/_layout/': { preLoaderRoute: typeof LayoutIndexImport parentRoute: typeof LayoutImport @@ -116,8 +116,8 @@ declare module '@tanstack/react-router' { export const routeTree = rootRoute.addChildren([ LayoutRoute.addChildren([ LayoutAdminRoute, - LayoutItemsRoute, LayoutSettingsRoute, + LayoutTodosRoute, LayoutIndexRoute, ]), LoginRoute, diff --git a/frontend/src/routes/_layout/items.tsx b/frontend/src/routes/_layout/items.tsx deleted file mode 100644 index 93f7ad5048..0000000000 --- a/frontend/src/routes/_layout/items.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { - Container, - Heading, - SkeletonText, - Table, - TableContainer, - Tbody, - Td, - Th, - Thead, - Tr, -} from "@chakra-ui/react" -import { useQuery, useQueryClient } from "@tanstack/react-query" -import { createFileRoute, useNavigate } from "@tanstack/react-router" -import { useEffect } from "react" -import { z } from "zod" - -import { ItemsService } from "../../client" -import ActionsMenu from "../../components/Common/ActionsMenu" -import Navbar from "../../components/Common/Navbar" -import AddItem from "../../components/Items/AddItem" -import { PaginationFooter } from "../../components/Common/PaginationFooter.tsx" - -const itemsSearchSchema = z.object({ - page: z.number().catch(1), -}) - -export const Route = createFileRoute("/_layout/items")({ - component: Items, - validateSearch: (search) => itemsSearchSchema.parse(search), -}) - -const PER_PAGE = 5 - -function getItemsQueryOptions({ page }: { page: number }) { - return { - queryFn: () => - ItemsService.readItems({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }), - queryKey: ["items", { page }], - } -} - -function ItemsTable() { - const queryClient = useQueryClient() - const { page } = Route.useSearch() - const navigate = useNavigate({ from: Route.fullPath }) - const setPage = (page: number) => - navigate({ search: (prev: {[key: string]: string}) => ({ ...prev, page }) }) - - const { - data: items, - isPending, - isPlaceholderData, - } = useQuery({ - ...getItemsQueryOptions({ page }), - placeholderData: (prevData) => prevData, - }) - - const hasNextPage = !isPlaceholderData && items?.data.length === PER_PAGE - const hasPreviousPage = page > 1 - - useEffect(() => { - if (hasNextPage) { - queryClient.prefetchQuery(getItemsQueryOptions({ page: page + 1 })) - } - }, [page, queryClient, hasNextPage]) - - return ( - <> - - - - - - - - - - - {isPending ? ( - - - {new Array(4).fill(null).map((_, index) => ( - - ))} - - - ) : ( - - {items?.data.map((item) => ( - - - - - - - ))} - - )} -
IDTitleDescriptionActions
- -
{item.id} - {item.title} - - {item.description || "N/A"} - - -
-
- - - ) -} - -function Items() { - return ( - - - Items Management - - - - - - ) -} diff --git a/frontend/src/routes/_layout/todos.tsx b/frontend/src/routes/_layout/todos.tsx new file mode 100644 index 0000000000..6f763e4b55 --- /dev/null +++ b/frontend/src/routes/_layout/todos.tsx @@ -0,0 +1,174 @@ +import { + Container, + Heading, + SkeletonText, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, +} from "@chakra-ui/react" +import { Button } from "../../components/ui/button" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { createFileRoute, useNavigate } from "@tanstack/react-router" +import { useEffect } from "react" +import { z } from "zod" + +import { TodosService } from "../../client/index.ts" +import ActionsMenu from "../../components/Common/ActionsMenu.tsx" +import Navbar from "../../components/Common/Navbar.tsx" +import AddTodo from "../../components/todos/Addtodos.tsx" +import { PaginationFooter } from "../../components/Common/PaginationFooter.tsx" + +const todosSearchSchema = z.object({ + page: z.number().catch(1), +}) + +export const Route = createFileRoute("/_layout/todos")({ + component: Todos, + validateSearch: (search) => todosSearchSchema.parse(search), +}) + +const PER_PAGE = 5 + +function getTodosQueryOptions({ page }: { page: number }) { + return { + queryFn: () => + TodosService.readTodos({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }), + queryKey: ["todos", { page }], + } +} + +function TodosTable() { + const queryClient = useQueryClient() + const { page } = Route.useSearch() + const navigate = useNavigate({ from: Route.fullPath }) + const setPage = (page: number) => + navigate({ search: (prev: {[key: string]: string}) => ({ ...prev, page }) }) + const { + data: items, + isPending, + isPlaceholderData, + } = useQuery({ + ...getTodosQueryOptions({ page }), + placeholderData: (prevData) => prevData, + }) + + const hasNextPage = !isPlaceholderData && items?.data.length === PER_PAGE + const hasPreviousPage = page > 1 + + useEffect(() => { + if (hasNextPage) { + queryClient.prefetchQuery(getTodosQueryOptions({ page: page + 1 })) + } + }, [page, queryClient, hasNextPage]) + + const changeStatus = async (todoId: string, newStatus: "pending" | "completed" | "in_progress") => { + try { + // Optimistic update + queryClient.setQueryData(["todos", { page }], (oldData: any) => { + if (!oldData) return oldData; + return { + ...oldData, + data: oldData.data.map((todo: any) => + todo.id === todoId ? { ...todo, status: newStatus } : todo + ), + }; + }); + + // Call API to persist changes + await TodosService.updateTodo({ id: todoId, requestBody: { status: newStatus } }); + + // Optionally refresh data after successful update + queryClient.invalidateQueries({ queryKey: ["todos"], exact: true, refetchType: "active" }); + } catch (error) { + console.error("Failed to change status", error); + } + }; + + return ( + <> + + + + + + + + + + + + {isPending ? ( + + + {new Array(4).fill(null).map((_, index) => ( + + ))} + + + ) : ( + + {items?.data.map((todo) => ( + + + + + + + + ))} + + )} +
IDTitleDescriptionStatusActions
+ +
{todo.id} + {todo.title} + + {todo.desc || "N/A"} + + {todo.status === "in_progress" ? ( + + ) : todo.status === "completed" ? ( + + ) : ( + + )} + + +
+
+ + + ) +} + +function Todos() { + return ( + + + To Do List Management + + + + + + ) +}