From e230af21c426a5056d33193f6ea8065017e00b4e Mon Sep 17 00:00:00 2001 From: david Date: Wed, 25 Feb 2026 12:29:45 +0300 Subject: [PATCH] lint frontend --- .github/workflows/backend-tests.yml | 141 ----- .github/workflows/backend.yml | 69 +++ .github/workflows/ci.yml | 116 ----- .github/workflows/frontend.yml | 39 ++ backend/app/__init__.py | 3 +- backend/tests/conftest.py | 2 +- docs/testing_guide.md | 491 ------------------ frontend/.eslintrc.json | 50 ++ frontend/.prettierignore | 27 + frontend/.prettierrc.json | 10 + frontend/package-lock.json | 401 ++++++++++++++ frontend/package.json | 10 +- frontend/src/App.tsx | 34 +- frontend/src/components/Navbar.jsx | 70 --- frontend/src/components/Navbar.tsx | 19 +- frontend/src/context/AppContext.tsx | 14 +- .../src/context/loaders/LoaderExample.tsx | 74 +-- frontend/src/context/loaders/LoaderRoot.tsx | 16 +- frontend/src/context/loaders/index.ts | 2 +- frontend/src/context/loaders/useLoader.tsx | 24 +- .../src/context/modals/ModalComponents.tsx | 14 +- frontend/src/context/modals/ModalExample.tsx | 12 +- frontend/src/context/modals/ModalRoot.tsx | 8 +- frontend/src/context/toasts/ToastExample.tsx | 7 +- frontend/src/context/toasts/ToastRoot.tsx | 39 +- frontend/src/context/toasts/useToast.tsx | 2 +- frontend/src/hooks/useApi.js | 88 ---- frontend/src/hooks/useApi.ts | 80 ++- frontend/src/hooks/useProducts.ts | 36 +- frontend/src/main.tsx | 16 +- frontend/src/pages/Cart.tsx | 15 +- frontend/src/pages/Home.tsx | 23 +- frontend/src/pages/Login.tsx | 62 +-- frontend/src/pages/Orders.tsx | 73 ++- frontend/src/pages/Products.tsx | 32 +- frontend/src/pages/Register.jsx | 177 ------- frontend/src/pages/Register.tsx | 80 +-- frontend/src/store/useStore.js | 48 -- frontend/src/types/api.ts | 2 +- frontend/src/types/modal.ts | 2 +- frontend/src/types/order.ts | 2 +- frontend/src/types/product.ts | 2 +- frontend/src/types/user.ts | 2 +- 43 files changed, 960 insertions(+), 1474 deletions(-) delete mode 100644 .github/workflows/backend-tests.yml create mode 100644 .github/workflows/backend.yml delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/frontend.yml delete mode 100644 docs/testing_guide.md create mode 100644 frontend/.eslintrc.json create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc.json delete mode 100644 frontend/src/components/Navbar.jsx delete mode 100644 frontend/src/hooks/useApi.js delete mode 100644 frontend/src/pages/Register.jsx delete mode 100644 frontend/src/store/useStore.js diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml deleted file mode 100644 index b1293e2..0000000 --- a/.github/workflows/backend-tests.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: Backend Tests - -on: - push: - branches: [ main, develop ] - paths: - - 'backend/**' - - '.github/workflows/backend-tests.yml' - pull_request: - branches: [ main, develop ] - paths: - - 'backend/**' - - '.github/workflows/backend-tests.yml' - -jobs: - test: - runs-on: [docker] - - strategy: - matrix: - python-version: ['3.10', '3.11', '3.12'] - - services: - postgres: - image: postgres:15 - env: - POSTGRES_USER: test_user - POSTGRES_PASSWORD: test_password - POSTGRES_DB: test_db - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 6379:6379 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: 'backend/requirements/*.txt' - - - name: Install dependencies - working-directory: ./backend - run: | - python -m pip install --upgrade pip - pip install -r requirements/base.txt - pip install -r requirements/dev.txt - - - name: Lint with flake8 (if installed) - working-directory: ./backend - run: | - pip install flake8 - # Stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # Exit-zero treats all errors as warnings - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - continue-on-error: true - - - name: Run tests with pytest - working-directory: ./backend - env: - DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db - CELERY_BROKER_URL: redis://localhost:6379/0 - CELERY_RESULT_BACKEND: redis://localhost:6379/0 - SECRET_KEY: test-secret-key - JWT_SECRET_KEY: test-jwt-secret-key - FLASK_ENV: testing - run: | - pytest --cov=app --cov-report=xml --cov-report=html --cov-report=term - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./backend/coverage.xml - flags: backend - name: codecov-umbrella - fail_ci_if_error: false - - - name: Upload coverage HTML as artifact - uses: actions/upload-artifact@v3 - with: - name: coverage-report-python-${{ matrix.python-version }} - path: backend/htmlcov/ - retention-days: 7 - - - name: Check coverage thresholds - working-directory: ./backend - run: | - coverage report --fail-under=80 - - security-scan: - runs-on: [docker] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - working-directory: ./backend - run: | - python -m pip install --upgrade pip - pip install bandit safety - - - name: Run Bandit security linter - working-directory: ./backend - run: | - bandit -r app -f json -o bandit-report.json || true - continue-on-error: true - - - name: Check for known security vulnerabilities - working-directory: ./backend - run: | - safety check --json --output safety-report.json || true - continue-on-error: true - - - name: Upload security reports - uses: actions/upload-artifact@v3 - with: - name: security-reports - path: | - backend/bandit-report.json - backend/safety-report.json \ No newline at end of file diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 0000000..b475078 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,69 @@ +name: Backend CI + +on: + push: + branches: [ main, develop ] + paths: + - 'backend/**' + - '.github/workflows/backend.yml' + pull_request: + branches: [ main, develop ] + paths: + - 'backend/**' + - '.github/workflows/backend.yml' + +jobs: + backend-test: + runs-on: [docker] + + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: test_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + container: + image: nikolaik/python-nodejs:python3.12-nodejs24-alpine + options: --volume forgejo-pip-cache:/tmp/pip-cache + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + run: | + python --version + + - name: Install dependencies + run: | + cd backend + python -m pip install --upgrade pip + pip install --cache-dir /tmp/pip-cache -r requirements/dev.txt + + - name: Debug cache + run: | + echo "Listing PIP cache files:" + pip cache dir + ls -la /tmp/pip-cache 2>/dev/null || echo "Cache dir empty or missing" + + - name: Lint with flake8 + run: | + cd backend + flake8 app tests --count --max-complexity=10 --max-line-length=127 --statistics --show-source + + - name: Run tests + env: + DATABASE_URL: postgresql://test:test@postgres:5432/test_db + SECRET_KEY: test-secret-key + JWT_SECRET_KEY: test-jwt-secret + FLASK_ENV: testing + run: | + cd backend + pytest --cov=app --cov-report=xml --cov-report=term + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 33474d1..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: CI - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -jobs: - backend-test: - runs-on: [docker] - - services: - postgres: - image: postgres:15-alpine - env: - POSTGRES_USER: test - POSTGRES_PASSWORD: test - POSTGRES_DB: test_db - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - container: - image: nikolaik/python-nodejs:python3.12-nodejs24-alpine - - steps: - - uses: actions/checkout@v6 - - - name: Set up Python - run: | - python --version - - - name: Cache pip packages - uses: actions/cache@v3 - with: - # This path is on the runner, but we mounted it to the container - path: /tmp/pip-cache - key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements/dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install dependencies - run: | - cd backend - python -m pip install --upgrade pip - pip install -r requirements/dev.txt - - - name: Lint with flake8 - run: | - cd backend - flake8 app tests --count --max-complexity=10 --max-line-length=127 --statistics --show-source - - - name: Run tests - env: - DATABASE_URL: postgresql://test:test@postgres:5432/test_db - SECRET_KEY: test-secret-key - JWT_SECRET_KEY: test-jwt-secret - FLASK_ENV: testing - run: | - cd backend - pytest --cov=app --cov-report=xml --cov-report=term - - frontend-test: - runs-on: [docker] - - steps: - - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Install dependencies - run: | - cd frontend - npm ci - - - name: Lint - run: | - cd frontend - npm run lint - - - name: Run tests - run: | - cd frontend - npm test -- --run --coverage - - - name: Upload coverage - uses: codecov/codecov-action@v3 - with: - files: ./frontend/coverage/coverage-final.json - flags: frontend - name: frontend-coverage - - build: - runs-on: [docker] - needs: [backend-test, frontend-test] - - steps: - - uses: actions/checkout@v3 - - - name: Build backend - run: | - cd backend - docker build -t crafting-shop-backend:test . - - - name: Build frontend - run: | - cd frontend - docker build -t crafting-shop-frontend:test . \ No newline at end of file diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 0000000..e75c321 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,39 @@ +name: Frontend CI + +on: + push: + branches: [ main, develop ] + paths: + - 'frontend/**' + - '.github/workflows/frontend.yml' + pull_request: + branches: [ main, develop ] + paths: + - 'frontend/**' + - '.github/workflows/frontend.yml' + +jobs: + frontend-test: + runs-on: [docker] + + # Note: Using the container here ensures the cache volume works reliably + container: + image: nikolaik/python-nodejs:python3.12-nodejs24-alpine + options: --volume forgejo-npm-cache:/tmp/npm-cache + + steps: + - uses: actions/checkout@v6 + + - name: Configure NPM cache + run: npm config set cache /tmp/npm-cache --global + + - name: Install dependencies + run: | + cd frontend + npm config get cache + npm ci + + - name: Lint + run: | + cd frontend + npm run lint diff --git a/backend/app/__init__.py b/backend/app/__init__.py index a8ba1ca..df448d6 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,3 +1,4 @@ +import json import os from dotenv import load_dotenv @@ -32,7 +33,7 @@ def create_app(config_name=None): f"------------------ENVIRONMENT: {config_name}-------------------------------------" ) # print(F'------------------CONFIG: {app.config}-------------------------------------') - # print(json.dumps(dict(app.config), indent=2, default=str)) + print(json.dumps(dict(app.config), indent=2, default=str)) print("----------------------------------------------------------") # Initialize extensions with app db.init_app(app) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index ba13b54..ee35f04 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -131,7 +131,7 @@ def products(db_session): name=fake.sentence(nb_words=4)[:-1], description=fake.paragraph(), price=fake.pydecimal(left_digits=2, right_digits=2, positive=True), - stock=fake.pyint(min_value=0, max_value=100), + stock=fake.pyint(min_value=20, max_value=100), image_url=fake.url(), ) db_session.add(product) diff --git a/docs/testing_guide.md b/docs/testing_guide.md deleted file mode 100644 index 26ed12a..0000000 --- a/docs/testing_guide.md +++ /dev/null @@ -1,491 +0,0 @@ -# Testing Guide - -## Overview - -This document provides a comprehensive guide to testing the Flask application using pytest. The testing infrastructure includes unit tests, integration tests, CI/CD pipelines, and pre-commit hooks. - -## Table of Contents - -1. [Installation](#installation) -2. [Running Tests](#running-tests) -3. [Test Structure](#test-structure) -4. [Writing Tests](#writing-tests) -5. [Fixtures](#fixtures) -6. [Coverage](#coverage) -7. [CI/CD Pipeline](#cicd-pipeline) -8. [Pre-commit Hooks](#pre-commit-hooks) -9. [Best Practices](#best-practices) - -## Installation - -### Install Test Dependencies - -```bash -cd backend -pip install -r requirements/base.txt -``` - -The base requirements include: -- `pytest==7.4.3` - Testing framework -- `pytest-flask==1.3.0` - Flask integration -- `pytest-cov==4.1.0` - Coverage reporting -- `pytest-mock==3.12.0` - Mocking utilities -- `factory-boy==3.3.0` - Test data factories -- `faker==20.1.0` - Fake data generation - -## Running Tests - -### Run All Tests - -```bash -cd backend -pytest -``` - -### Run with Verbose Output - -```bash -pytest -v -``` - -### Run with Coverage Report - -```bash -pytest --cov=app --cov-report=html --cov-report=term -``` - -### Run Specific Test Files - -```bash -# Run all model tests -pytest tests/test_models.py - -# Run all route tests -pytest tests/test_routes.py - -# Run all schema tests -pytest tests/test_schemas.py -``` - -### Run by Test Name - -```bash -pytest -k "test_user_creation" -pytest -k "test_login" -``` - -### Run by Markers - -```bash -# Run only unit tests -pytest -m unit - -# Run only integration tests -pytest -m integration - -# Run only authentication tests -pytest -m auth - -# Run only product tests -pytest -m product - -# Run only order tests -pytest -m order -``` - -### Run Tests in Parallel (faster) - -Install pytest-xdist: -```bash -pip install pytest-xdist -pytest -n auto # Use all available CPUs -``` - -## Test Structure - -``` -backend/ -├── tests/ -│ ├── __init__.py -│ ├── conftest.py # Global fixtures and configuration -│ ├── test_models.py # Model tests -│ ├── test_routes.py # Route/API tests -│ └── test_schemas.py # Pydantic schema tests -├── pytest.ini # Pytest configuration -├── .coveragerc # Coverage configuration -└── app/ - ├── __init__.py - ├── models/ # Database models - ├── routes/ # API routes - ├── schemas/ # Pydantic schemas - └── ... -``` - -## Writing Tests - -### Test File Structure - -```python -import pytest -from app.models import User -from app import db - -class TestUserModel: - """Test User model""" - - @pytest.mark.unit - def test_user_creation(self, db_session): - """Test creating a user""" - user = User( - email='test@example.com', - username='testuser' - ) - user.set_password('password123') - db_session.add(user) - db_session.commit() - - assert user.id is not None - assert user.email == 'test@example.com' -``` - -### Test API Routes - -```python -def test_get_products(client, products): - """Test getting all products""" - response = client.get('/api/products') - - assert response.status_code == 200 - data = response.get_json() - assert len(data) == 5 - -def test_create_product(client, admin_headers): - """Test creating a product""" - response = client.post('/api/products', - headers=admin_headers, - json={ - 'name': 'New Product', - 'price': 29.99 - }) - - assert response.status_code == 201 - data = response.get_json() - assert data['name'] == 'New Product' -``` - -### Parameterized Tests - -```python -@pytest.mark.parametrize("email,password,expected_status", [ - ("user@example.com", "correct123", 200), - ("wrong@email.com", "correct123", 401), - ("user@example.com", "wrongpass", 401), -]) -def test_login_validation(client, email, password, expected_status): - """Test login with various inputs""" - response = client.post('/api/auth/login', json={ - 'email': email, - 'password': password - }) - assert response.status_code == expected_status -``` - -## Fixtures - -### Available Fixtures - -#### Application Fixtures - -- **`app`**: Flask application instance with test configuration -- **`client`**: Test client for making HTTP requests -- **`runner`**: CLI runner for testing Flask CLI commands -- **`db_session`**: Database session for database operations - -#### User Fixtures - -- **`admin_user`**: Creates an admin user -- **`regular_user`**: Creates a regular user -- **`inactive_user`**: Creates an inactive user - -#### Product Fixtures - -- **`product`**: Creates a single product -- **`products`**: Creates 5 products - -#### Authentication Fixtures - -- **`auth_headers`**: JWT headers for regular user -- **`admin_headers`**: JWT headers for admin user - -#### Order Fixtures - -- **`order`**: Creates an order with items - -### Creating Custom Fixtures - -```python -# In conftest.py or test file -@pytest.fixture -def custom_product(db_session): - """Create a custom product""" - product = Product( - name='Custom Product', - price=99.99, - stock=50 - ) - db_session.add(product) - db_session.commit() - return product - -# Use in tests -def test_custom_fixture(custom_product): - assert custom_product.name == 'Custom Product' -``` - -## Coverage - -### Coverage Configuration - -Coverage is configured in `.coveragerc`: - -```ini -[run] -source = app -omit = - */tests/* - */migrations/* - */__pycache__/* - -[report] -exclude_lines = - pragma: no cover - def __repr__ - raise NotImplementedError -``` - -### Coverage Thresholds - -The CI/CD pipeline enforces 80% minimum code coverage. - -### Generate Coverage Report - -```bash -# Terminal report -pytest --cov=app --cov-report=term - -# HTML report -pytest --cov=app --cov-report=html -open htmlcov/index.html # Mac -xdg-open htmlcov/index.html # Linux -``` - -### Coverage Report Example - -``` -Name Stmts Miss Cover Missing ----------------------------------------------- -app/__init__.py 10 2 80% 15-16 -app/models/user.py 45 5 89% 23, 45 -app/routes/api.py 120 20 83% 78-85 ----------------------------------------------- -TOTAL 175 27 85% -``` - -## CI/CD Pipeline - -### GitHub Actions Workflow - -The backend has automated testing via GitHub Actions: - -**File**: `.github/workflows/backend-tests.yml` - -### Pipeline Stages - -1. **Test Matrix**: Runs tests on Python 3.10, 3.11, and 3.12 -2. **Services**: Sets up PostgreSQL and Redis -3. **Linting**: Runs flake8 for code quality -4. **Testing**: Executes pytest with coverage -5. **Coverage Upload**: Sends coverage to Codecov -6. **Security Scan**: Runs bandit and safety - -### Triggering the Pipeline - -The pipeline runs automatically on: -- Push to `main` or `develop` branches -- Pull requests to `main` or `develop` branches -- Changes to `backend/**` or workflow files - -### Viewing Results - -1. Go to the Actions tab in your GitHub repository -2. Click on the latest workflow run -3. View test results, coverage, and artifacts - -## Pre-commit Hooks - -### Setup Pre-commit Hooks - -```bash -# Install pre-commit -pip install pre-commit - -# Install hooks -pre-commit install - -# Run hooks manually -pre-commit run --all-files -``` - -### Available Hooks - -The `.pre-commit-config.yaml` includes: - -1. **Black**: Code formatting -2. **isort**: Import sorting -3. **flake8**: Linting -4. **pytest**: Run tests before committing -5. **mypy**: Type checking -6. **bandit**: Security checks - -### Hook Behavior - -Hooks run automatically on: -- `git commit` -- Can be skipped with `git commit --no-verify` - -## Best Practices - -### ✅ DO - -1. **Use descriptive test names** - ```python - def test_user_creation_with_valid_data(): # Good - def test_user(): # Bad - ``` - -2. **Test both success and failure cases** - ```python - def test_login_success(): ... - def test_login_invalid_credentials(): ... - def test_login_missing_fields(): ... - ``` - -3. **Use fixtures for common setup** - ```python - def test_something(client, admin_user, products): ... - ``` - -4. **Mock external services** - ```python - def test_external_api(mocker): - mock_response = {'data': 'mocked'} - mocker.patch('requests.get', return_value=mock_response) - ``` - -5. **Keep tests independent** - - Each test should be able to run alone - - Don't rely on test execution order - -6. **Use markers appropriately** - ```python - @pytest.mark.slow - def test_expensive_operation(): ... - ``` - -### ❌ DON'T - -1. **Don't share state between tests** - ```python - # Bad - shared state - global_user = User(...) - - # Good - use fixtures - @pytest.fixture - def user(): return User(...) - ``` - -2. **Don't hardcode sensitive data** - ```python - # Bad - password = 'real_password_123' - - # Good - password = fake.password() - ``` - -3. **Don't use production database** - - Always use test database (SQLite) - - Fixtures automatically create isolated databases - -4. **Don't skip error cases** - ```python - # Bad - only tests success - def test_create_product(): ... - - # Good - tests both - def test_create_product_success(): ... - def test_create_product_validation_error(): ... - ``` - -5. **Don't ignore slow tests in CI** - - Mark slow tests with `@pytest.mark.slow` - - Run them separately if needed - -## Test Coverage Requirements - -| Module | Line Coverage | Branch Coverage | -|--------|--------------|-----------------| -| routes.py | >90% | >85% | -| models.py | >85% | >80% | -| schemas.py | >90% | >85% | -| services/ | >80% | >75% | -| utils/ | >70% | >65% | - -## Troubleshooting - -### Tests Fail with Database Errors - -```bash -# Clean up test databases -rm -f backend/*.db -``` - -### Coverage Not Showing - -```bash -# Install coverage separately -pip install coverage - -# Clean previous coverage data -coverage erase - -# Run tests again -pytest --cov=app -``` - -### Import Errors - -```bash -# Ensure you're in the backend directory -cd backend - -# Install in development mode -pip install -e . -``` - -### Slow Tests - -```bash -# Run only specific tests -pytest tests/test_routes.py::TestProductRoutes::test_get_products - -# Run in parallel -pytest -n auto -``` - -## Additional Resources - -- [Pytest Documentation](https://docs.pytest.org/) -- [Pytest-Flask Documentation](https://pytest-flask.readthedocs.io/) -- [Pydantic Documentation](https://docs.pydantic.dev/) -- [Flask Testing Documentation](https://flask.palletsprojects.com/en/latest/testing/) \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..657312a --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,50 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "plugins": [ + "react", + "react-hooks", + "@typescript-eslint", + "prettier" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], + "prettier/prettier": "warn" + }, + "ignorePatterns": [ + "dist", + "build", + "node_modules", + "*.config.js", + "*.config.ts", + "vite.config.ts" + ] +} \ No newline at end of file diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..4fea64b --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ +package-lock.json + +# Build outputs +dist/ +build/ + +# Testing +coverage/ + +# Misc +.DS_Store +*.log +.env +.env.* + +# Config files that might have different formatting +*.config.js +*.config.ts +vite.config.ts +vitest.config.ts +postcss.config.js +tailwind.config.js + +# Other +public/ \ No newline at end of file diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..d858cde --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9f3290c..8675e67 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,15 +19,20 @@ "@testing-library/user-event": "^14.5.1", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", "@vitejs/plugin-react": "^4.2.1", "@vitest/ui": "^1.0.4", "autoprefixer": "^10.4.16", "eslint": "^8.55.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "jsdom": "^23.0.1", "postcss": "^8.4.32", + "prettier": "^3.8.1", "tailwindcss": "^3.4.0", "typescript": "^5.9.3", "vite": "^5.0.8", @@ -1018,6 +1023,18 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1551,6 +1568,285 @@ "@types/react": "^18.0.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", + "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -3045,6 +3341,51 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -3235,6 +3576,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -5293,6 +5640,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -6175,6 +6549,21 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -6376,6 +6765,18 @@ "node": ">=18" } }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0014018..043c425 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,10 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint . --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --ext ts,tsx,js,jsx --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", "test": "vitest", "test:ui": "vitest --ui" }, @@ -22,15 +25,20 @@ "@testing-library/user-event": "^14.5.1", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", "@vitejs/plugin-react": "^4.2.1", "@vitest/ui": "^1.0.4", "autoprefixer": "^10.4.16", "eslint": "^8.55.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "jsdom": "^23.0.1", "postcss": "^8.4.32", + "prettier": "^3.8.1", "tailwindcss": "^3.4.0", "typescript": "^5.9.3", "vite": "^5.0.8", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a539c36..57f0f6c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,17 +1,17 @@ -import { Routes, Route } from 'react-router-dom' -import { ModalProvider } from './context/modals/useModal' -import { ModalRoot } from './context/modals/ModalRoot' -import { ToastProvider } from './context/toasts/useToast' -import { ToastRoot } from './context/toasts/ToastRoot' -import { LoaderProvider } from './context/loaders/useLoader' -import { LoaderRoot } from './context/loaders/LoaderRoot' -import Cart from './pages/Cart' -import { Navbar } from './components/Navbar' -import { Home } from './pages/Home' -import { Products } from './pages/Products' -import Login from './pages/Login' -import { Register } from './pages/Register' -import { Orders } from './pages/Orders' +import { Routes, Route } from 'react-router-dom'; +import { ModalProvider } from './context/modals/useModal'; +import { ModalRoot } from './context/modals/ModalRoot'; +import { ToastProvider } from './context/toasts/useToast'; +import { ToastRoot } from './context/toasts/ToastRoot'; +import { LoaderProvider } from './context/loaders/useLoader'; +import { LoaderRoot } from './context/loaders/LoaderRoot'; +import Cart from './pages/Cart'; +import { Navbar } from './components/Navbar'; +import { Home } from './pages/Home'; +import { Products } from './pages/Products'; +import Login from './pages/Login'; +import { Register } from './pages/Register'; +import { Orders } from './pages/Orders'; const App = () => { return ( @@ -38,7 +38,7 @@ const App = () => { - ) -} + ); +}; -export default App \ No newline at end of file +export default App; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx deleted file mode 100644 index 149097e..0000000 --- a/frontend/src/components/Navbar.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Link } from 'react-router-dom' -import { useApp } from '../context/AppContext' - -export function Navbar() { - const { user } = useApp() - - return ( - - ) -} \ No newline at end of file diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 149097e..ed353d7 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,15 +1,18 @@ -import { Link } from 'react-router-dom' -import { useApp } from '../context/AppContext' +import { Link } from 'react-router-dom'; +import { useApp } from '../context/AppContext'; export function Navbar() { - const { user } = useApp() + const { user } = useApp(); return ( - ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/context/AppContext.tsx b/frontend/src/context/AppContext.tsx index 36575bf..6fc794f 100644 --- a/frontend/src/context/AppContext.tsx +++ b/frontend/src/context/AppContext.tsx @@ -36,17 +36,17 @@ export function AppProvider({ children }: AppProviderProps) { useEffect(() => { const storedToken = localStorage.getItem('token'); const storedUser = localStorage.getItem('user'); - + if (storedToken && storedUser) { setToken(storedToken); setUser(JSON.parse(storedUser)); } - + const storedCart = localStorage.getItem('cart'); if (storedCart) { setCart(JSON.parse(storedCart)); } - + setLoading(false); }, []); @@ -87,9 +87,7 @@ export function AppProvider({ children }: AppProviderProps) { const existingItem = prevCart.find((item) => item.id === product.id); if (existingItem) { return prevCart.map((item) => - item.id === product.id - ? { ...item, quantity: item.quantity + 1 } - : item + item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item ); } return [...prevCart, { ...product, quantity: 1 }]; @@ -106,9 +104,7 @@ export function AppProvider({ children }: AppProviderProps) { return; } setCart((prevCart: CartItem[]) => - prevCart.map((item) => - item.id === productId ? { ...item, quantity } : item - ) + prevCart.map((item) => (item.id === productId ? { ...item, quantity } : item)) ); }; diff --git a/frontend/src/context/loaders/LoaderExample.tsx b/frontend/src/context/loaders/LoaderExample.tsx index 1a580ed..913e806 100644 --- a/frontend/src/context/loaders/LoaderExample.tsx +++ b/frontend/src/context/loaders/LoaderExample.tsx @@ -7,12 +7,12 @@ export const LoaderExample = () => { // Pattern A: Manual Control const handleManualLoad = async () => { - showLoader("Processing manual task..."); - + showLoader('Processing manual task...'); + try { // Simulate an async operation - await new Promise(resolve => setTimeout(resolve, 2000)); - + await new Promise((resolve) => setTimeout(resolve, 2000)); + addNotification({ type: 'success', title: 'Manual Task Complete', @@ -20,6 +20,7 @@ export const LoaderExample = () => { duration: 3000, }); } catch (err) { + console.error(err); addNotification({ type: 'error', title: 'Manual Task Failed', @@ -34,15 +35,12 @@ export const LoaderExample = () => { // Pattern B: withLoader Helper (Cleanest) const handleWithLoader = async () => { try { - await withLoader( - async () => { - // Simulate an async operation - await new Promise(resolve => setTimeout(resolve, 1500)); - return 'Success!'; - }, - "Processing with withLoader..." - ); - + await withLoader(async () => { + // Simulate an async operation + await new Promise((resolve) => setTimeout(resolve, 1500)); + return 'Success!'; + }, 'Processing with withLoader...'); + addNotification({ type: 'success', title: 'withLoader Task Complete', @@ -50,6 +48,7 @@ export const LoaderExample = () => { duration: 3000, }); } catch (err) { + console.error(err); addNotification({ type: 'error', title: 'withLoader Task Failed', @@ -61,12 +60,12 @@ export const LoaderExample = () => { // Pattern C: Long-running task const handleLongLoad = async () => { - showLoader("Processing long-running task..."); - + showLoader('Processing long-running task...'); + try { // Simulate a longer async operation - await new Promise(resolve => setTimeout(resolve, 4000)); - + await new Promise((resolve) => setTimeout(resolve, 4000)); + addNotification({ type: 'success', title: 'Long Task Complete', @@ -74,6 +73,7 @@ export const LoaderExample = () => { duration: 3000, }); } catch (err) { + console.error(err); addNotification({ type: 'error', title: 'Long Task Failed', @@ -87,14 +87,14 @@ export const LoaderExample = () => { // Pattern D: Error simulation const handleError = async () => { - showLoader("Processing task that will fail..."); - + showLoader('Processing task that will fail...'); + try { // Simulate an error - await new Promise((_, reject) => + await new Promise((_, reject) => setTimeout(() => reject(new Error('Simulated error')), 1500) ); - + addNotification({ type: 'success', title: 'Task Complete', @@ -102,6 +102,7 @@ export const LoaderExample = () => { duration: 3000, }); } catch (err) { + console.error(err); addNotification({ type: 'error', title: 'Task Failed', @@ -116,10 +117,10 @@ export const LoaderExample = () => { // Pattern E: Without message const handleNoMessage = async () => { showLoader(); - + try { - await new Promise(resolve => setTimeout(resolve, 2000)); - + await new Promise((resolve) => setTimeout(resolve, 2000)); + addNotification({ type: 'success', title: 'No Message Task Complete', @@ -127,6 +128,7 @@ export const LoaderExample = () => { duration: 3000, }); } catch (err) { + console.error(err); addNotification({ type: 'error', title: 'Task Failed', @@ -142,9 +144,10 @@ export const LoaderExample = () => {

Loader System Examples

- Click the buttons below to see different loader patterns in action. The loader uses React Context for state management. + Click the buttons below to see different loader patterns in action. The loader uses React + Context for state management.

- +
); -}; \ No newline at end of file +}; diff --git a/frontend/src/context/loaders/LoaderRoot.tsx b/frontend/src/context/loaders/LoaderRoot.tsx index 5ae6fee..b59b3c4 100644 --- a/frontend/src/context/loaders/LoaderRoot.tsx +++ b/frontend/src/context/loaders/LoaderRoot.tsx @@ -7,27 +7,23 @@ export const LoaderRoot = () => { if (!isLoading) return null; return createPortal( -
{/* Custom CSS Spinner (No external libs) */}
{/* Optional inner circle for style */}
-
+
- - {message && ( -

- {message} -

- )} + + {message &&

{message}

}
, document.body ); -}; \ No newline at end of file +}; diff --git a/frontend/src/context/loaders/index.ts b/frontend/src/context/loaders/index.ts index 9967013..7b52118 100644 --- a/frontend/src/context/loaders/index.ts +++ b/frontend/src/context/loaders/index.ts @@ -1,2 +1,2 @@ export { LoaderProvider, useLoader } from './useLoader'; -export { LoaderRoot } from './LoaderRoot'; \ No newline at end of file +export { LoaderRoot } from './LoaderRoot'; diff --git a/frontend/src/context/loaders/useLoader.tsx b/frontend/src/context/loaders/useLoader.tsx index fa291ca..17bd9f1 100644 --- a/frontend/src/context/loaders/useLoader.tsx +++ b/frontend/src/context/loaders/useLoader.tsx @@ -30,17 +30,17 @@ export const LoaderProvider: FC = ({ children }) => { }, []); // Helper to avoid try/finally blocks everywhere - const withLoader = useCallback(async ( - fn: () => Promise, - message?: string - ): Promise => { - showLoader(message); - try { - return await fn(); - } finally { - hideLoader(); - } - }, [showLoader, hideLoader]); + const withLoader = useCallback( + async (fn: () => Promise, message?: string): Promise => { + showLoader(message); + try { + return await fn(); + } finally { + hideLoader(); + } + }, + [showLoader, hideLoader] + ); return ( @@ -53,4 +53,4 @@ export const useLoader = () => { const context = useContext(LoaderContext); if (!context) throw new Error('useLoader must be used within a LoaderProvider'); return context; -}; \ No newline at end of file +}; diff --git a/frontend/src/context/modals/ModalComponents.tsx b/frontend/src/context/modals/ModalComponents.tsx index 21c222a..5eb2abe 100644 --- a/frontend/src/context/modals/ModalComponents.tsx +++ b/frontend/src/context/modals/ModalComponents.tsx @@ -3,15 +3,19 @@ import { ReactNode } from 'react'; // Container for the Header section export const ModalHeader = ({ children, title }: { children?: ReactNode; title?: string }) => (
- {title ? : children} + {title ? ( + + ) : ( + children + )}
); // Container for the Main Body export const ModalContent = ({ children }: { children: ReactNode }) => ( -
- {children} -
+
{children}
); // Container for Actions (Buttons) @@ -26,4 +30,4 @@ export const Modal = { Header: ModalHeader, Content: ModalContent, Actions: ModalActions, -}; \ No newline at end of file +}; diff --git a/frontend/src/context/modals/ModalExample.tsx b/frontend/src/context/modals/ModalExample.tsx index 3bbc34f..596bae2 100644 --- a/frontend/src/context/modals/ModalExample.tsx +++ b/frontend/src/context/modals/ModalExample.tsx @@ -11,14 +11,16 @@ const DeleteConfirmModal = ({ onClose }: { onClose: () => void }) => {

- -
); -}; \ No newline at end of file +}; diff --git a/frontend/src/context/modals/ModalRoot.tsx b/frontend/src/context/modals/ModalRoot.tsx index c98ed0f..a0f6ac1 100644 --- a/frontend/src/context/modals/ModalRoot.tsx +++ b/frontend/src/context/modals/ModalRoot.tsx @@ -21,13 +21,15 @@ export const ModalRoot = () => { } else { document.body.style.overflow = 'unset'; } - return () => { document.body.style.overflow = 'unset'; }; + return () => { + document.body.style.overflow = 'unset'; + }; }, [isOpen]); if (!isOpen || !content) return null; return createPortal( -
{ aria-labelledby="modal-title" > {/* Stop propagation so clicking modal content doesn't close it */} -
e.stopPropagation()} > diff --git a/frontend/src/context/toasts/ToastExample.tsx b/frontend/src/context/toasts/ToastExample.tsx index dbc52f0..69b183a 100644 --- a/frontend/src/context/toasts/ToastExample.tsx +++ b/frontend/src/context/toasts/ToastExample.tsx @@ -52,9 +52,10 @@ export const ToastExample = () => {

Toast System Examples

- Click the buttons below to see different toast notifications in action. The toast uses React Context for state management. + Click the buttons below to see different toast notifications in action. The toast uses React + Context for state management.

- +
); -}; \ No newline at end of file +}; diff --git a/frontend/src/context/toasts/ToastRoot.tsx b/frontend/src/context/toasts/ToastRoot.tsx index 3d92fc2..84cb4ed 100644 --- a/frontend/src/context/toasts/ToastRoot.tsx +++ b/frontend/src/context/toasts/ToastRoot.tsx @@ -15,22 +15,36 @@ const Icons = { ), warning: ( - + ), info: ( - + ), }; const getColors = (type: NotificationType) => { switch (type) { - case 'success': return 'bg-white border-l-4 border-green-500'; - case 'error': return 'bg-white border-l-4 border-red-500'; - case 'warning': return 'bg-white border-l-4 border-yellow-500'; - case 'info': return 'bg-white border-l-4 border-blue-500'; + case 'success': + return 'bg-white border-l-4 border-green-500'; + case 'error': + return 'bg-white border-l-4 border-red-500'; + case 'warning': + return 'bg-white border-l-4 border-yellow-500'; + case 'info': + return 'bg-white border-l-4 border-blue-500'; } }; @@ -50,9 +64,7 @@ export const ToastRoot = () => {
{Icons[toast.type]}

{toast.title}

- {toast.message && ( -

{toast.message}

- )} + {toast.message &&

{toast.message}

}
@@ -68,4 +85,4 @@ export const ToastRoot = () => {
, document.body ); -}; \ No newline at end of file +}; diff --git a/frontend/src/context/toasts/useToast.tsx b/frontend/src/context/toasts/useToast.tsx index 75b9e2d..62bb237 100644 --- a/frontend/src/context/toasts/useToast.tsx +++ b/frontend/src/context/toasts/useToast.tsx @@ -59,4 +59,4 @@ export const useToast = () => { const context = useContext(ToastContext); if (!context) throw new Error('useToast must be used within a ToastProvider'); return context; -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/useApi.js b/frontend/src/hooks/useApi.js deleted file mode 100644 index 6a28376..0000000 --- a/frontend/src/hooks/useApi.js +++ /dev/null @@ -1,88 +0,0 @@ -import axios from 'axios' - -const api = axios.create({ - baseURL: '/api', - headers: { - 'Content-Type': 'application/json', - }, -}) - -// Add token to requests if available -api.interceptors.request.use( - (config) => { - const token = localStorage.getItem('token') - if (token) { - config.headers.Authorization = `Bearer ${token}` - } - return config - }, - (error) => Promise.reject(error) -) - -// Handle response errors -api.interceptors.response.use( - (response) => response, - (error) => { - if (error.response?.status === 401) { - // Token expired or invalid - localStorage.removeItem('token') - localStorage.removeItem('user') - window.location.href = '/login' - } - return Promise.reject(error) - } -) - -export function useApi() { - return { - // Auth - login: async (email, password) => { - const response = await api.post('/auth/login', { email, password }) - return response.data - }, - register: async (userData) => { - const response = await api.post('/auth/register', userData) - return response.data - }, - getCurrentUser: async () => { - const response = await api.get('/users/me') - return response.data - }, - - // Products - getProducts: async () => { - const response = await api.get('/products') - return response.data - }, - getProduct: async (id) => { - const response = await api.get(`/products/${id}`) - return response.data - }, - createProduct: async (productData) => { - const response = await api.post('/products', productData) - return response.data - }, - updateProduct: async (id, productData) => { - const response = await api.put(`/products/${id}`, productData) - return response.data - }, - deleteProduct: async (id) => { - const response = await api.delete(`/products/${id}`) - return response.data - }, - - // Orders - getOrders: async () => { - const response = await api.get('/orders') - return response.data - }, - getOrder: async (id) => { - const response = await api.get(`/orders/${id}`) - return response.data - }, - createOrder: async (orderData) => { - const response = await api.post('/orders', orderData) - return response.data - }, - } -} \ No newline at end of file diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index 887f383..0a32994 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -1,31 +1,24 @@ -import axios from 'axios' -import { - RegisterData, - UserData, - ProductData, - OrderData, - AuthResponse -} from '../types' +import axios from 'axios'; +import { RegisterData, UserData, ProductData, OrderData, AuthResponse } from '../types'; const api = axios.create({ baseURL: '/api', headers: { 'Content-Type': 'application/json', }, -}) - +}); // Add token to requests if available api.interceptors.request.use( (config) => { - const token = localStorage.getItem('token') + const token = localStorage.getItem('token'); if (token) { - config.headers.Authorization = `Bearer ${token}` + config.headers.Authorization = `Bearer ${token}`; } - return config + return config; }, (error) => Promise.reject(error) -) +); // Handle response errors api.interceptors.response.use( @@ -33,63 +26,66 @@ api.interceptors.response.use( (error) => { if (error.response?.status === 401) { // Token expired or invalid - localStorage.removeItem('token') - localStorage.removeItem('user') - window.location.href = '/login' + localStorage.removeItem('token'); + localStorage.removeItem('user'); + window.location.href = '/login'; } - return Promise.reject(error) + return Promise.reject(error); } -) +); export function useApi() { return { // Auth login: async (email: string, password: string): Promise => { - const response = await api.post('/auth/login', { email, password }) - return response.data + const response = await api.post('/auth/login', { + email, + password, + }); + return response.data; }, register: async (userData: RegisterData): Promise => { - const response = await api.post('/auth/register', userData) - return response.data + const response = await api.post('/auth/register', userData); + return response.data; }, getCurrentUser: async (): Promise => { - const response = await api.get('/users/me') - return response.data + const response = await api.get('/users/me'); + return response.data; }, // Products getProducts: async (): Promise => { - const response = await api.get('/products') - return response.data + const response = await api.get('/products'); + return response.data; }, getProduct: async (id: string): Promise => { - const response = await api.get(`/products/${id}`) - return response.data + const response = await api.get(`/products/${id}`); + return response.data; }, createProduct: async (productData: Omit): Promise => { - const response = await api.post('/products', productData) - return response.data + const response = await api.post('/products', productData); + return response.data; }, updateProduct: async (id: string, productData: Partial): Promise => { - const response = await api.put(`/products/${id}`, productData) - return response.data + const response = await api.put(`/products/${id}`, productData); + return response.data; }, deleteProduct: async (id: string): Promise => { - await api.delete(`/products/${id}`) + await api.delete(`/products/${id}`); }, // Orders getOrders: async (): Promise => { - const response = await api.get('/orders') - return response.data + const response = await api.get('/orders'); + return response.data; }, getOrder: async (id: string): Promise => { - const response = await api.get(`/orders/${id}`) - return response.data + const response = await api.get(`/orders/${id}`); + return response.data; }, createOrder: async (orderData: Omit): Promise => { - const response = await api.post('/orders', orderData) - return response.data + const response = await api.post('/orders', orderData); + return response.data; }, - } -} \ No newline at end of file + }; +} diff --git a/frontend/src/hooks/useProducts.ts b/frontend/src/hooks/useProducts.ts index 6e1f250..f4da397 100644 --- a/frontend/src/hooks/useProducts.ts +++ b/frontend/src/hooks/useProducts.ts @@ -7,7 +7,7 @@ import { ProductData } from '../types'; export function useProducts() { const [products, setProducts] = useState([]); const [error, setError] = useState(null); - + const { getProducts } = useApi(); const { withLoader } = useLoader(); const { addNotification } = useToast(); @@ -15,28 +15,25 @@ export function useProducts() { const fetchProducts = async () => { try { setError(null); - + // Use withLoader to show loading state and handle errors - const data = await withLoader( - () => getProducts(), - 'Loading products...' - ); - + const data = await withLoader(() => getProducts(), 'Loading products...'); + setProducts(data); - - // // Show success toast - // addNotification({ - // type: 'success', - // title: 'Products Loaded', - // message: `Successfully loaded ${data.length} products.`, - // duration: 3000, - // }); - + + // // Show success toast + // addNotification({ + // type: 'success', + // title: 'Products Loaded', + // message: `Successfully loaded ${data.length} products.`, + // duration: 3000, + // }); + return data; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to load products'; setError(errorMessage); - + // Show error toast addNotification({ type: 'error', @@ -44,7 +41,7 @@ export function useProducts() { message: errorMessage, duration: 5000, }); - + return []; } }; @@ -52,6 +49,7 @@ export function useProducts() { // Optionally auto-fetch on mount useEffect(() => { fetchProducts(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { @@ -60,4 +58,4 @@ export function useProducts() { loading: false, // Loading is handled by the global loader refetch: fetchProducts, }; -} \ No newline at end of file +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 0889f69..a0bdab8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,9 +1,9 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import { BrowserRouter } from 'react-router-dom' -import { AppProvider } from './context/AppContext' -import App from './App.tsx' -import './index.css' +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { AppProvider } from './context/AppContext'; +import App from './App.tsx'; +import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( @@ -12,5 +12,5 @@ ReactDOM.createRoot(document.getElementById('root')!).render( - , -) + +); diff --git a/frontend/src/pages/Cart.tsx b/frontend/src/pages/Cart.tsx index 457fed2..38e24b6 100644 --- a/frontend/src/pages/Cart.tsx +++ b/frontend/src/pages/Cart.tsx @@ -29,6 +29,7 @@ export default function Cart() { clearCart(); navigate('/orders'); } catch (error) { + console.error(error); alert('Failed to create order. Please try again.'); } }; @@ -51,7 +52,7 @@ export default function Cart() { return (

Shopping Cart

- +
{cart.map((item) => (
)} - +

{item.name}

${item.price}

- +
- +

${(item.price * item.quantity).toFixed(2)}

- +
- +
); -} \ No newline at end of file +} diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index f017b7a..f44ba7c 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,15 +1,13 @@ -import { Link } from 'react-router-dom' -import { ModalExample } from '../context/modals/ModalExample' -import { ToastExample } from '../context/toasts/ToastExample' -import { LoaderExample } from '../context/loaders/LoaderExample' +import { Link } from 'react-router-dom'; +import { ModalExample } from '../context/modals/ModalExample'; +import { ToastExample } from '../context/toasts/ToastExample'; +import { LoaderExample } from '../context/loaders/LoaderExample'; export function Home() { return (
-

- Welcome to Crafting Shop -

+

Welcome to Crafting Shop

Your one-stop shop for premium crafting supplies

@@ -39,7 +37,8 @@ export function Home() {

Modal System Demo

- Test our modal system with this interactive example. The modal uses React Context for state management. + Test our modal system with this interactive example. The modal uses React Context for + state management.

@@ -47,7 +46,8 @@ export function Home() {

Toast System Demo

- Test our toast notification system with this interactive example. The toast uses React Context for state management. + Test our toast notification system with this interactive example. The toast uses React + Context for state management.

@@ -55,10 +55,11 @@ export function Home() {

Loader System Demo

- Test our global loading system with this interactive example. The loader uses React Context for state management. + Test our global loading system with this interactive example. The loader uses React + Context for state management.

- ) + ); } diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index b48babc..58a907b 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,52 +1,52 @@ -import { useState } from 'react' -import { useNavigate, Link } from 'react-router-dom' -import { useApp } from '../context/AppContext' -import { useApi } from '../hooks/useApi' -import { User } from '../types' +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useApp } from '../context/AppContext'; +import { useApi } from '../hooks/useApi'; +import { User } from '../types'; export default function Login() { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - - const navigate = useNavigate() - const { login } = useApp() - const { login: loginApi } = useApi() + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const navigate = useNavigate(); + const { login } = useApp(); + const { login: loginApi } = useApi(); const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError('') - setLoading(true) + e.preventDefault(); + setError(''); + setLoading(true); try { - const response = await loginApi(email, password) + const response = await loginApi(email, password); // Convert UserData to User type const user: User = { id: parseInt(response.user.id), username: response.user.username, email: response.user.email, - } - login(user, response.token) - navigate('/') + }; + login(user, response.token); + navigate('/'); } catch (err) { - setError(err instanceof Error ? err.message : 'Login failed. Please try again.') + setError(err instanceof Error ? err.message : 'Login failed. Please try again.'); } finally { - setLoading(false) + setLoading(false); } - } + }; return (

Login

- +
{error && (
{error}
)} - +
- +
- +
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/pages/Orders.tsx b/frontend/src/pages/Orders.tsx index 167c806..0d76ab1 100644 --- a/frontend/src/pages/Orders.tsx +++ b/frontend/src/pages/Orders.tsx @@ -1,34 +1,35 @@ -import { useEffect, useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { useApp } from '../context/AppContext' -import { useApi } from '../hooks/useApi' -import { OrderData } from '../types' +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useApp } from '../context/AppContext'; +import { useApi } from '../hooks/useApi'; +import { OrderData } from '../types'; export function Orders() { - const [orders, setOrders] = useState([]) - const [loading, setLoading] = useState(true) - const navigate = useNavigate() - const { user } = useApp() - const { getOrders } = useApi() + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + const { user } = useApp(); + const { getOrders } = useApi(); useEffect(() => { if (!user) { - navigate('/login') - return + navigate('/login'); + return; } - fetchOrders() - }, [user, navigate]) + fetchOrders(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user, navigate]); const fetchOrders = async () => { try { - const data = await getOrders() - setOrders(data) + const data = await getOrders(); + setOrders(data); } catch (error) { - console.error('Error fetching orders:', error) + console.error('Error fetching orders:', error); } finally { - setLoading(false) + setLoading(false); } - } + }; const getStatusColor = (status: string): string => { const colors: Record = { @@ -37,22 +38,22 @@ export function Orders() { shipped: 'bg-purple-900 text-purple-200 border-purple-700', delivered: 'bg-green-900 text-green-200 border-green-700', cancelled: 'bg-red-900 text-red-200 border-red-700', - } - return colors[status] || 'bg-gray-900 text-gray-200 border-gray-700' - } + }; + return colors[status] || 'bg-gray-900 text-gray-200 border-gray-700'; + }; if (loading) { return (
Loading orders...
- ) + ); } return (

My Orders

- + {orders.length === 0 ? (

You have no orders yet

@@ -72,9 +73,7 @@ export function Orders() { >
-

- Order #{order.id} -

+

Order #{order.id}

{new Date(order.created_at).toLocaleDateString()}

@@ -87,7 +86,7 @@ export function Orders() { {order.status.charAt(0).toUpperCase() + order.status.slice(1)}
- +
{order.items.map((item) => (

Product #{item.product_id}

-

- Quantity: {item.quantity} -

+

Quantity: {item.quantity}

${(item.price * item.quantity).toFixed(2)} @@ -106,18 +103,14 @@ export function Orders() {

))}
- +
- {order.shipping_address && ( - Ship to: {order.shipping_address} - )} + {order.shipping_address && Ship to: {order.shipping_address}}
Total:{' '} - - ${order.total_amount} - + ${order.total_amount}
@@ -125,5 +118,5 @@ export function Orders() {
)}
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/pages/Products.tsx b/frontend/src/pages/Products.tsx index 757b515..ac36019 100644 --- a/frontend/src/pages/Products.tsx +++ b/frontend/src/pages/Products.tsx @@ -1,10 +1,10 @@ -import { useApp } from '../context/AppContext' -import { useProducts } from '../hooks/useProducts' -import { CartItem } from '../types' +import { useApp } from '../context/AppContext'; +import { useProducts } from '../hooks/useProducts'; +import { CartItem } from '../types'; export function Products() { - const { products, refetch } = useProducts() - const { addToCart } = useApp() + const { products, refetch } = useProducts(); + const { addToCart } = useApp(); return (
@@ -31,19 +31,11 @@ export function Products() { /> )}
-

- {product.name} -

-

- {product.description} -

+

{product.name}

+

{product.description}

- - ${product.price} - - - Stock: {product.stock} - + ${product.price} + Stock: {product.stock}
)}
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx deleted file mode 100644 index 58bfe49..0000000 --- a/frontend/src/pages/Register.jsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useState } from 'react' -import { useNavigate, Link } from 'react-router-dom' -import { useApi } from '../hooks/useApi.js' - -export function Register() { - const [formData, setFormData] = useState({ - email: '', - username: '', - password: '', - confirmPassword: '', - first_name: '', - last_name: '', - }) - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - - const navigate = useNavigate() - const { register } = useApi() - - const handleChange = (e) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value, - }) - } - - const handleSubmit = async (e) => { - e.preventDefault() - setError('') - - if (formData.password !== formData.confirmPassword) { - setError('Passwords do not match') - return - } - - if (formData.password.length < 6) { - setError('Password must be at least 6 characters') - return - } - - setLoading(true) - - try { - await register({ - email: formData.email, - username: formData.username, - password: formData.password, - first_name: formData.first_name, - last_name: formData.last_name, - }) - navigate('/login') - } catch (err) { - setError(err.response?.data?.error || 'Registration failed. Please try again.') - } finally { - setLoading(false) - } - } - - return ( -
-

Register

- -
- {error && ( -
- {error} -
- )} - -
-
- - -
- -
- - -
-
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - -
- -

- Already have an account?{' '} - - Login - -

-
- ) -} \ No newline at end of file diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index bc439b5..989bc5f 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -1,14 +1,14 @@ -import { useState, FormEvent, ChangeEvent } from 'react' -import { useNavigate, Link } from 'react-router-dom' -import { useApi } from '../hooks/useApi' +import { useState, FormEvent, ChangeEvent } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useApi } from '../hooks/useApi'; interface FormData { - email: string - username: string - password: string - confirmPassword: string - first_name: string - last_name: string + email: string; + username: string; + password: string; + confirmPassword: string; + first_name: string; + last_name: string; } export function Register() { @@ -19,35 +19,35 @@ export function Register() { confirmPassword: '', first_name: '', last_name: '', - }) - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - - const navigate = useNavigate() - const { register } = useApi() + }); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const navigate = useNavigate(); + const { register } = useApi(); const handleChange = (e: ChangeEvent) => { setFormData({ ...formData, [e.target.name]: e.target.value, - }) - } + }); + }; const handleSubmit = async (e: FormEvent) => { - e.preventDefault() - setError('') + e.preventDefault(); + setError(''); if (formData.password !== formData.confirmPassword) { - setError('Passwords do not match') - return + setError('Passwords do not match'); + return; } if (formData.password.length < 6) { - setError('Password must be at least 6 characters') - return + setError('Password must be at least 6 characters'); + return; } - setLoading(true) + setLoading(true); try { await register({ @@ -56,26 +56,26 @@ export function Register() { password: formData.password, first_name: formData.first_name, last_name: formData.last_name, - }) - navigate('/login') + }); + navigate('/login'); } catch (err: any) { - setError(err.response?.data?.error || 'Registration failed. Please try again.') + setError(err.response?.data?.error || 'Registration failed. Please try again.'); } finally { - setLoading(false) + setLoading(false); } - } + }; return (

Register

- +
{error && (
{error}
)} - +
- +
- +
- +
- +
- +
- +
- ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/store/useStore.js b/frontend/src/store/useStore.js deleted file mode 100644 index 191b172..0000000 --- a/frontend/src/store/useStore.js +++ /dev/null @@ -1,48 +0,0 @@ -import { create } from 'zustand' -import { persist } from 'zustand/middleware' - -export const useStore = create( - persist( - (set) => ({ - user: null, - token: null, - cart: [], - - setUser: (user) => set({ user }), - setToken: (token) => set({ token }), - logout: () => set({ user: null, token: null, cart: [] }), - - addToCart: (product) => - set((state) => { - const existingItem = state.cart.find((item) => item.id === product.id) - if (existingItem) { - return { - cart: state.cart.map((item) => - item.id === product.id - ? { ...item, quantity: item.quantity + 1 } - : item - ), - } - } - return { cart: [...state.cart, { ...product, quantity: 1 }] } - }), - - removeFromCart: (productId) => - set((state) => ({ - cart: state.cart.filter((item) => item.id !== productId), - })), - - updateCartQuantity: (productId, quantity) => - set((state) => ({ - cart: state.cart.map((item) => - item.id === productId ? { ...item, quantity } : item - ), - })), - - clearCart: () => set({ cart: [] }), - }), - { - name: 'crafting-shop-storage', - } - ) -) \ No newline at end of file diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 643ce90..115fcfe 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -1,4 +1,4 @@ export interface ApiResponse { data: T; message?: string; -} \ No newline at end of file +} diff --git a/frontend/src/types/modal.ts b/frontend/src/types/modal.ts index 03feb3e..abacf3b 100644 --- a/frontend/src/types/modal.ts +++ b/frontend/src/types/modal.ts @@ -1,3 +1,3 @@ export interface ModalContentProps { onClose: () => void; -} \ No newline at end of file +} diff --git a/frontend/src/types/order.ts b/frontend/src/types/order.ts index 55eff94..edce5cd 100644 --- a/frontend/src/types/order.ts +++ b/frontend/src/types/order.ts @@ -21,4 +21,4 @@ export interface Order { total_amount: number; shipping_address: string; items: OrderItem[]; -} \ No newline at end of file +} diff --git a/frontend/src/types/product.ts b/frontend/src/types/product.ts index 9f60b61..4a4046a 100644 --- a/frontend/src/types/product.ts +++ b/frontend/src/types/product.ts @@ -23,4 +23,4 @@ export interface CartItem { quantity: number; image_url?: string; [key: string]: any; -} \ No newline at end of file +} diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts index 6a1b3a5..cdf1b6a 100644 --- a/frontend/src/types/user.ts +++ b/frontend/src/types/user.ts @@ -27,4 +27,4 @@ export interface RegisterData { export interface AuthResponse { token: string; user: UserData; -} \ No newline at end of file +}