lint frontend

This commit is contained in:
david 2026-02-25 12:29:45 +03:00
parent 1c35df931a
commit e230af21c4
43 changed files with 960 additions and 1474 deletions

View file

@ -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

69
.github/workflows/backend.yml vendored Normal file
View file

@ -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

View file

@ -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 .

39
.github/workflows/frontend.yml vendored Normal file
View file

@ -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

View file

@ -1,3 +1,4 @@
import json
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
@ -32,7 +33,7 @@ def create_app(config_name=None):
f"------------------ENVIRONMENT: {config_name}-------------------------------------" f"------------------ENVIRONMENT: {config_name}-------------------------------------"
) )
# print(F'------------------CONFIG: {app.config}-------------------------------------') # 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("----------------------------------------------------------") print("----------------------------------------------------------")
# Initialize extensions with app # Initialize extensions with app
db.init_app(app) db.init_app(app)

View file

@ -131,7 +131,7 @@ def products(db_session):
name=fake.sentence(nb_words=4)[:-1], name=fake.sentence(nb_words=4)[:-1],
description=fake.paragraph(), description=fake.paragraph(),
price=fake.pydecimal(left_digits=2, right_digits=2, positive=True), 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(), image_url=fake.url(),
) )
db_session.add(product) db_session.add(product)

View file

@ -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/)

50
frontend/.eslintrc.json Normal file
View file

@ -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"
]
}

27
frontend/.prettierignore Normal file
View file

@ -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/

10
frontend/.prettierrc.json Normal file
View file

@ -0,0 +1,10 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "always",
"endOfLine": "lf"
}

View file

@ -19,15 +19,20 @@
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
"@types/react": "^18.3.28", "@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7", "@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", "@vitejs/plugin-react": "^4.2.1",
"@vitest/ui": "^1.0.4", "@vitest/ui": "^1.0.4",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"eslint": "^8.55.0", "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": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"jsdom": "^23.0.1", "jsdom": "^23.0.1",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"prettier": "^3.8.1",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^5.0.8", "vite": "^5.0.8",
@ -1018,6 +1023,18 @@
"node": ">= 8" "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": { "node_modules/@polka/url": {
"version": "1.0.0-next.29", "version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
@ -1551,6 +1568,285 @@
"@types/react": "^18.0.0" "@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": { "node_modules/@ungap/structured-clone": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@ -3045,6 +3341,51 @@
"url": "https://opencollective.com/eslint" "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": { "node_modules/eslint-plugin-react": {
"version": "7.37.5", "version": "7.37.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", "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==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true "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": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@ -5293,6 +5640,33 @@
"node": ">= 0.8.0" "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": { "node_modules/pretty-format": {
"version": "27.5.1", "version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "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==", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true "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": { "node_modules/tailwindcss": {
"version": "3.4.19", "version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
@ -6376,6 +6765,18 @@
"node": ">=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": { "node_modules/ts-interface-checker": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",

View file

@ -6,7 +6,10 @@
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "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": "vitest",
"test:ui": "vitest --ui" "test:ui": "vitest --ui"
}, },
@ -22,15 +25,20 @@
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
"@types/react": "^18.3.28", "@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7", "@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", "@vitejs/plugin-react": "^4.2.1",
"@vitest/ui": "^1.0.4", "@vitest/ui": "^1.0.4",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"eslint": "^8.55.0", "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": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"jsdom": "^23.0.1", "jsdom": "^23.0.1",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"prettier": "^3.8.1",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^5.0.8", "vite": "^5.0.8",

View file

@ -1,17 +1,17 @@
import { Routes, Route } from 'react-router-dom' import { Routes, Route } from 'react-router-dom';
import { ModalProvider } from './context/modals/useModal' import { ModalProvider } from './context/modals/useModal';
import { ModalRoot } from './context/modals/ModalRoot' import { ModalRoot } from './context/modals/ModalRoot';
import { ToastProvider } from './context/toasts/useToast' import { ToastProvider } from './context/toasts/useToast';
import { ToastRoot } from './context/toasts/ToastRoot' import { ToastRoot } from './context/toasts/ToastRoot';
import { LoaderProvider } from './context/loaders/useLoader' import { LoaderProvider } from './context/loaders/useLoader';
import { LoaderRoot } from './context/loaders/LoaderRoot' import { LoaderRoot } from './context/loaders/LoaderRoot';
import Cart from './pages/Cart' import Cart from './pages/Cart';
import { Navbar } from './components/Navbar' import { Navbar } from './components/Navbar';
import { Home } from './pages/Home' import { Home } from './pages/Home';
import { Products } from './pages/Products' import { Products } from './pages/Products';
import Login from './pages/Login' import Login from './pages/Login';
import { Register } from './pages/Register' import { Register } from './pages/Register';
import { Orders } from './pages/Orders' import { Orders } from './pages/Orders';
const App = () => { const App = () => {
return ( return (
@ -38,7 +38,7 @@ const App = () => {
</ModalProvider> </ModalProvider>
</ToastProvider> </ToastProvider>
</LoaderProvider> </LoaderProvider>
) );
} };
export default App export default App;

View file

@ -1,70 +0,0 @@
import { Link } from 'react-router-dom'
import { useApp } from '../context/AppContext'
export function Navbar() {
const { user } = useApp()
return (
<nav className="bg-gray-800 border-b border-gray-700 shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<Link to="/" className="text-xl font-bold text-white hover:text-blue-400 transition-colors">
Crafting Shop
</Link>
<div className="ml-10 flex items-baseline space-x-4">
<Link
to="/"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Home
</Link>
<Link
to="/products"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Products
</Link>
<Link
to="/cart"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Cart
</Link>
{user && (
<Link
to="/orders"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Orders
</Link>
)}
</div>
</div>
<div className="flex items-center">
{user ? (
<span className="text-gray-300 px-3 py-2">
{user.username}
</span>
) : (
<>
<Link
to="/login"
className="text-gray-300 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors"
>
Login
</Link>
<Link
to="/register"
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
>
Register
</Link>
</>
)}
</div>
</div>
</div>
</nav>
)
}

View file

@ -1,15 +1,18 @@
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom';
import { useApp } from '../context/AppContext' import { useApp } from '../context/AppContext';
export function Navbar() { export function Navbar() {
const { user } = useApp() const { user } = useApp();
return ( return (
<nav className="bg-gray-800 border-b border-gray-700 shadow-md"> <nav className="bg-gray-800 border-b border-gray-700 shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16"> <div className="flex items-center justify-between h-16">
<div className="flex items-center"> <div className="flex items-center">
<Link to="/" className="text-xl font-bold text-white hover:text-blue-400 transition-colors"> <Link
to="/"
className="text-xl font-bold text-white hover:text-blue-400 transition-colors"
>
Crafting Shop Crafting Shop
</Link> </Link>
<div className="ml-10 flex items-baseline space-x-4"> <div className="ml-10 flex items-baseline space-x-4">
@ -43,9 +46,7 @@ export function Navbar() {
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
{user ? ( {user ? (
<span className="text-gray-300 px-3 py-2"> <span className="text-gray-300 px-3 py-2">{user.username}</span>
{user.username}
</span>
) : ( ) : (
<> <>
<Link <Link
@ -66,5 +67,5 @@ export function Navbar() {
</div> </div>
</div> </div>
</nav> </nav>
) );
} }

View file

@ -36,17 +36,17 @@ export function AppProvider({ children }: AppProviderProps) {
useEffect(() => { useEffect(() => {
const storedToken = localStorage.getItem('token'); const storedToken = localStorage.getItem('token');
const storedUser = localStorage.getItem('user'); const storedUser = localStorage.getItem('user');
if (storedToken && storedUser) { if (storedToken && storedUser) {
setToken(storedToken); setToken(storedToken);
setUser(JSON.parse(storedUser)); setUser(JSON.parse(storedUser));
} }
const storedCart = localStorage.getItem('cart'); const storedCart = localStorage.getItem('cart');
if (storedCart) { if (storedCart) {
setCart(JSON.parse(storedCart)); setCart(JSON.parse(storedCart));
} }
setLoading(false); setLoading(false);
}, []); }, []);
@ -87,9 +87,7 @@ export function AppProvider({ children }: AppProviderProps) {
const existingItem = prevCart.find((item) => item.id === product.id); const existingItem = prevCart.find((item) => item.id === product.id);
if (existingItem) { if (existingItem) {
return prevCart.map((item) => return prevCart.map((item) =>
item.id === product.id item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
? { ...item, quantity: item.quantity + 1 }
: item
); );
} }
return [...prevCart, { ...product, quantity: 1 }]; return [...prevCart, { ...product, quantity: 1 }];
@ -106,9 +104,7 @@ export function AppProvider({ children }: AppProviderProps) {
return; return;
} }
setCart((prevCart: CartItem[]) => setCart((prevCart: CartItem[]) =>
prevCart.map((item) => prevCart.map((item) => (item.id === productId ? { ...item, quantity } : item))
item.id === productId ? { ...item, quantity } : item
)
); );
}; };

View file

@ -7,12 +7,12 @@ export const LoaderExample = () => {
// Pattern A: Manual Control // Pattern A: Manual Control
const handleManualLoad = async () => { const handleManualLoad = async () => {
showLoader("Processing manual task..."); showLoader('Processing manual task...');
try { try {
// Simulate an async operation // Simulate an async operation
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
addNotification({ addNotification({
type: 'success', type: 'success',
title: 'Manual Task Complete', title: 'Manual Task Complete',
@ -20,6 +20,7 @@ export const LoaderExample = () => {
duration: 3000, duration: 3000,
}); });
} catch (err) { } catch (err) {
console.error(err);
addNotification({ addNotification({
type: 'error', type: 'error',
title: 'Manual Task Failed', title: 'Manual Task Failed',
@ -34,15 +35,12 @@ export const LoaderExample = () => {
// Pattern B: withLoader Helper (Cleanest) // Pattern B: withLoader Helper (Cleanest)
const handleWithLoader = async () => { const handleWithLoader = async () => {
try { try {
await withLoader( await withLoader(async () => {
async () => { // Simulate an async operation
// Simulate an async operation await new Promise((resolve) => setTimeout(resolve, 1500));
await new Promise(resolve => setTimeout(resolve, 1500)); return 'Success!';
return 'Success!'; }, 'Processing with withLoader...');
},
"Processing with withLoader..."
);
addNotification({ addNotification({
type: 'success', type: 'success',
title: 'withLoader Task Complete', title: 'withLoader Task Complete',
@ -50,6 +48,7 @@ export const LoaderExample = () => {
duration: 3000, duration: 3000,
}); });
} catch (err) { } catch (err) {
console.error(err);
addNotification({ addNotification({
type: 'error', type: 'error',
title: 'withLoader Task Failed', title: 'withLoader Task Failed',
@ -61,12 +60,12 @@ export const LoaderExample = () => {
// Pattern C: Long-running task // Pattern C: Long-running task
const handleLongLoad = async () => { const handleLongLoad = async () => {
showLoader("Processing long-running task..."); showLoader('Processing long-running task...');
try { try {
// Simulate a longer async operation // Simulate a longer async operation
await new Promise(resolve => setTimeout(resolve, 4000)); await new Promise((resolve) => setTimeout(resolve, 4000));
addNotification({ addNotification({
type: 'success', type: 'success',
title: 'Long Task Complete', title: 'Long Task Complete',
@ -74,6 +73,7 @@ export const LoaderExample = () => {
duration: 3000, duration: 3000,
}); });
} catch (err) { } catch (err) {
console.error(err);
addNotification({ addNotification({
type: 'error', type: 'error',
title: 'Long Task Failed', title: 'Long Task Failed',
@ -87,14 +87,14 @@ export const LoaderExample = () => {
// Pattern D: Error simulation // Pattern D: Error simulation
const handleError = async () => { const handleError = async () => {
showLoader("Processing task that will fail..."); showLoader('Processing task that will fail...');
try { try {
// Simulate an error // Simulate an error
await new Promise((_, reject) => await new Promise((_, reject) =>
setTimeout(() => reject(new Error('Simulated error')), 1500) setTimeout(() => reject(new Error('Simulated error')), 1500)
); );
addNotification({ addNotification({
type: 'success', type: 'success',
title: 'Task Complete', title: 'Task Complete',
@ -102,6 +102,7 @@ export const LoaderExample = () => {
duration: 3000, duration: 3000,
}); });
} catch (err) { } catch (err) {
console.error(err);
addNotification({ addNotification({
type: 'error', type: 'error',
title: 'Task Failed', title: 'Task Failed',
@ -116,10 +117,10 @@ export const LoaderExample = () => {
// Pattern E: Without message // Pattern E: Without message
const handleNoMessage = async () => { const handleNoMessage = async () => {
showLoader(); showLoader();
try { try {
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
addNotification({ addNotification({
type: 'success', type: 'success',
title: 'No Message Task Complete', title: 'No Message Task Complete',
@ -127,6 +128,7 @@ export const LoaderExample = () => {
duration: 3000, duration: 3000,
}); });
} catch (err) { } catch (err) {
console.error(err);
addNotification({ addNotification({
type: 'error', type: 'error',
title: 'Task Failed', title: 'Task Failed',
@ -142,9 +144,10 @@ export const LoaderExample = () => {
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-xl font-semibold text-white mb-4">Loader System Examples</h3> <h3 className="text-xl font-semibold text-white mb-4">Loader System Examples</h3>
<p className="text-gray-400 mb-6"> <p className="text-gray-400 mb-6">
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.
</p> </p>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<button <button
onClick={handleManualLoad} onClick={handleManualLoad}
@ -181,12 +184,21 @@ export const LoaderExample = () => {
<div className="mt-6 p-4 bg-gray-700 rounded-lg border border-gray-600"> <div className="mt-6 p-4 bg-gray-700 rounded-lg border border-gray-600">
<h4 className="text-white font-semibold mb-2">Usage Tips:</h4> <h4 className="text-white font-semibold mb-2">Usage Tips:</h4>
<ul className="text-gray-300 text-sm space-y-1 list-disc list-inside"> <ul className="text-gray-300 text-sm space-y-1 list-disc list-inside">
<li><strong>Manual Control:</strong> Use showLoader/hideLoader when you need fine-grained control</li> <li>
<li><strong>withLoader:</strong> Use the helper for automatic cleanup (recommended)</li> <strong>Manual Control:</strong> Use showLoader/hideLoader when you need fine-grained
<li><strong>Message:</strong> Optional - provides context about what's happening</li> control
<li><strong>Z-Index:</strong> Loader (70) sits above Toasts (60) and Modals (50)</li> </li>
<li>
<strong>withLoader:</strong> Use the helper for automatic cleanup (recommended)
</li>
<li>
<strong>Message:</strong> Optional - provides context about what is happening
</li>
<li>
<strong>Z-Index:</strong> Loader (70) sits above Toasts (60) and Modals (50)
</li>
</ul> </ul>
</div> </div>
</div> </div>
); );
}; };

View file

@ -7,27 +7,23 @@ export const LoaderRoot = () => {
if (!isLoading) return null; if (!isLoading) return null;
return createPortal( return createPortal(
<div <div
className="fixed inset-0 z-[70] flex flex-col items-center justify-center bg-black/60 backdrop-blur-sm transition-opacity duration-200" className="fixed inset-0 z-[70] flex flex-col items-center justify-center bg-black/60 backdrop-blur-sm transition-opacity duration-200"
role="alert" role="alert"
aria-busy="true" aria-busy="true"
aria-label={message || "Loading"} aria-label={message || 'Loading'}
> >
{/* Custom CSS Spinner (No external libs) */} {/* Custom CSS Spinner (No external libs) */}
<div className="relative"> <div className="relative">
<div className="w-16 h-16 border-4 border-white/30 border-t-white rounded-full animate-spin"></div> <div className="w-16 h-16 border-4 border-white/30 border-t-white rounded-full animate-spin"></div>
{/* Optional inner circle for style */} {/* Optional inner circle for style */}
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<div className="w-8 h-8 border-4 border-white/50 border-b-transparent rounded-full animate-spin"></div> <div className="w-8 h-8 border-4 border-white/50 border-b-transparent rounded-full animate-spin"></div>
</div> </div>
</div> </div>
{message && ( {message && <p className="mt-4 text-white font-medium text-lg animate-pulse">{message}</p>}
<p className="mt-4 text-white font-medium text-lg animate-pulse">
{message}
</p>
)}
</div>, </div>,
document.body document.body
); );
}; };

View file

@ -1,2 +1,2 @@
export { LoaderProvider, useLoader } from './useLoader'; export { LoaderProvider, useLoader } from './useLoader';
export { LoaderRoot } from './LoaderRoot'; export { LoaderRoot } from './LoaderRoot';

View file

@ -30,17 +30,17 @@ export const LoaderProvider: FC<LoaderProviderProps> = ({ children }) => {
}, []); }, []);
// Helper to avoid try/finally blocks everywhere // Helper to avoid try/finally blocks everywhere
const withLoader = useCallback(async <T,>( const withLoader = useCallback(
fn: () => Promise<T>, async <T,>(fn: () => Promise<T>, message?: string): Promise<T> => {
message?: string showLoader(message);
): Promise<T> => { try {
showLoader(message); return await fn();
try { } finally {
return await fn(); hideLoader();
} finally { }
hideLoader(); },
} [showLoader, hideLoader]
}, [showLoader, hideLoader]); );
return ( return (
<LoaderContext.Provider value={{ ...state, showLoader, hideLoader, withLoader }}> <LoaderContext.Provider value={{ ...state, showLoader, hideLoader, withLoader }}>
@ -53,4 +53,4 @@ export const useLoader = () => {
const context = useContext(LoaderContext); const context = useContext(LoaderContext);
if (!context) throw new Error('useLoader must be used within a LoaderProvider'); if (!context) throw new Error('useLoader must be used within a LoaderProvider');
return context; return context;
}; };

View file

@ -3,15 +3,19 @@ import { ReactNode } from 'react';
// Container for the Header section // Container for the Header section
export const ModalHeader = ({ children, title }: { children?: ReactNode; title?: string }) => ( export const ModalHeader = ({ children, title }: { children?: ReactNode; title?: string }) => (
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50"> <div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
{title ? <h3 id="modal-title" className="text-lg font-semibold text-gray-900">{title}</h3> : children} {title ? (
<h3 id="modal-title" className="text-lg font-semibold text-gray-900">
{title}
</h3>
) : (
children
)}
</div> </div>
); );
// Container for the Main Body // Container for the Main Body
export const ModalContent = ({ children }: { children: ReactNode }) => ( export const ModalContent = ({ children }: { children: ReactNode }) => (
<div className="px-6 py-4 overflow-y-auto max-h-[60vh]"> <div className="px-6 py-4 overflow-y-auto max-h-[60vh]">{children}</div>
{children}
</div>
); );
// Container for Actions (Buttons) // Container for Actions (Buttons)
@ -26,4 +30,4 @@ export const Modal = {
Header: ModalHeader, Header: ModalHeader,
Content: ModalContent, Content: ModalContent,
Actions: ModalActions, Actions: ModalActions,
}; };

View file

@ -11,14 +11,16 @@ const DeleteConfirmModal = ({ onClose }: { onClose: () => void }) => {
</p> </p>
</Modal.Content> </Modal.Content>
<Modal.Actions> <Modal.Actions>
<button <button
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50" className="px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={() => { /* Delete logic here */ onClose(); }} onClick={() => {
/* Delete logic here */ onClose();
}}
className="px-4 py-2 text-white bg-red-600 rounded hover:bg-red-700" className="px-4 py-2 text-white bg-red-600 rounded hover:bg-red-700"
> >
Delete Delete
@ -39,7 +41,7 @@ export const ModalExample = () => {
return ( return (
<div className="p-10"> <div className="p-10">
<h1 className="text-2xl font-bold mb-4">Modal System Example</h1> <h1 className="text-2xl font-bold mb-4">Modal System Example</h1>
<button <button
onClick={handleDeleteClick} onClick={handleDeleteClick}
className="px-6 py-3 text-white bg-blue-600 rounded hover:bg-blue-700" className="px-6 py-3 text-white bg-blue-600 rounded hover:bg-blue-700"
> >
@ -47,4 +49,4 @@ export const ModalExample = () => {
</button> </button>
</div> </div>
); );
}; };

View file

@ -21,13 +21,15 @@ export const ModalRoot = () => {
} else { } else {
document.body.style.overflow = 'unset'; document.body.style.overflow = 'unset';
} }
return () => { document.body.style.overflow = 'unset'; }; return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]); }, [isOpen]);
if (!isOpen || !content) return null; if (!isOpen || !content) return null;
return createPortal( return createPortal(
<div <div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
onClick={closeModal} // Click outside to close onClick={closeModal} // Click outside to close
role="dialog" role="dialog"
@ -35,7 +37,7 @@ export const ModalRoot = () => {
aria-labelledby="modal-title" aria-labelledby="modal-title"
> >
{/* Stop propagation so clicking modal content doesn't close it */} {/* Stop propagation so clicking modal content doesn't close it */}
<div <div
className="bg-white rounded-lg shadow-xl w-full max-w-md overflow-hidden flex flex-col transform transition-all" className="bg-white rounded-lg shadow-xl w-full max-w-md overflow-hidden flex flex-col transform transition-all"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >

View file

@ -52,9 +52,10 @@ export const ToastExample = () => {
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-xl font-semibold text-white mb-4">Toast System Examples</h3> <h3 className="text-xl font-semibold text-white mb-4">Toast System Examples</h3>
<p className="text-gray-400 mb-6"> <p className="text-gray-400 mb-6">
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.
</p> </p>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<button <button
onClick={showSuccess} onClick={showSuccess}
@ -89,4 +90,4 @@ export const ToastExample = () => {
</div> </div>
</div> </div>
); );
}; };

View file

@ -15,22 +15,36 @@ const Icons = {
), ),
warning: ( warning: (
<svg className="w-5 h-5 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-5 h-5 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg> </svg>
), ),
info: ( info: (
<svg className="w-5 h-5 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-5 h-5 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
), ),
}; };
const getColors = (type: NotificationType) => { const getColors = (type: NotificationType) => {
switch (type) { switch (type) {
case 'success': return 'bg-white border-l-4 border-green-500'; case 'success':
case 'error': return 'bg-white border-l-4 border-red-500'; return 'bg-white border-l-4 border-green-500';
case 'warning': return 'bg-white border-l-4 border-yellow-500'; case 'error':
case 'info': return 'bg-white border-l-4 border-blue-500'; 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 = () => {
<div className="flex-shrink-0 mt-0.5">{Icons[toast.type]}</div> <div className="flex-shrink-0 mt-0.5">{Icons[toast.type]}</div>
<div className="flex-1"> <div className="flex-1">
<h4 className="text-sm font-semibold text-gray-900">{toast.title}</h4> <h4 className="text-sm font-semibold text-gray-900">{toast.title}</h4>
{toast.message && ( {toast.message && <p className="text-sm text-gray-600 mt-1">{toast.message}</p>}
<p className="text-sm text-gray-600 mt-1">{toast.message}</p>
)}
</div> </div>
<button <button
onClick={() => removeNotification(toast.id)} onClick={() => removeNotification(toast.id)}
@ -60,7 +72,12 @@ export const ToastRoot = () => {
aria-label="Close notification" aria-label="Close notification"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
</div> </div>
@ -68,4 +85,4 @@ export const ToastRoot = () => {
</div>, </div>,
document.body document.body
); );
}; };

View file

@ -59,4 +59,4 @@ export const useToast = () => {
const context = useContext(ToastContext); const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within a ToastProvider'); if (!context) throw new Error('useToast must be used within a ToastProvider');
return context; return context;
}; };

View file

@ -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
},
}
}

View file

@ -1,31 +1,24 @@
import axios from 'axios' import axios from 'axios';
import { import { RegisterData, UserData, ProductData, OrderData, AuthResponse } from '../types';
RegisterData,
UserData,
ProductData,
OrderData,
AuthResponse
} from '../types'
const api = axios.create({ const api = axios.create({
baseURL: '/api', baseURL: '/api',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}) });
// Add token to requests if available // Add token to requests if available
api.interceptors.request.use( api.interceptors.request.use(
(config) => { (config) => {
const token = localStorage.getItem('token') const token = localStorage.getItem('token');
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}` config.headers.Authorization = `Bearer ${token}`;
} }
return config return config;
}, },
(error) => Promise.reject(error) (error) => Promise.reject(error)
) );
// Handle response errors // Handle response errors
api.interceptors.response.use( api.interceptors.response.use(
@ -33,63 +26,66 @@ api.interceptors.response.use(
(error) => { (error) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
// Token expired or invalid // Token expired or invalid
localStorage.removeItem('token') localStorage.removeItem('token');
localStorage.removeItem('user') localStorage.removeItem('user');
window.location.href = '/login' window.location.href = '/login';
} }
return Promise.reject(error) return Promise.reject(error);
} }
) );
export function useApi() { export function useApi() {
return { return {
// Auth // Auth
login: async (email: string, password: string): Promise<AuthResponse> => { login: async (email: string, password: string): Promise<AuthResponse> => {
const response = await api.post<AuthResponse>('/auth/login', { email, password }) const response = await api.post<AuthResponse>('/auth/login', {
return response.data email,
password,
});
return response.data;
}, },
register: async (userData: RegisterData): Promise<AuthResponse> => { register: async (userData: RegisterData): Promise<AuthResponse> => {
const response = await api.post<AuthResponse>('/auth/register', userData) const response = await api.post<AuthResponse>('/auth/register', userData);
return response.data return response.data;
}, },
getCurrentUser: async (): Promise<UserData> => { getCurrentUser: async (): Promise<UserData> => {
const response = await api.get<UserData>('/users/me') const response = await api.get<UserData>('/users/me');
return response.data return response.data;
}, },
// Products // Products
getProducts: async (): Promise<ProductData[]> => { getProducts: async (): Promise<ProductData[]> => {
const response = await api.get<ProductData[]>('/products') const response = await api.get<ProductData[]>('/products');
return response.data return response.data;
}, },
getProduct: async (id: string): Promise<ProductData> => { getProduct: async (id: string): Promise<ProductData> => {
const response = await api.get<ProductData>(`/products/${id}`) const response = await api.get<ProductData>(`/products/${id}`);
return response.data return response.data;
}, },
createProduct: async (productData: Omit<ProductData, 'id'>): Promise<ProductData> => { createProduct: async (productData: Omit<ProductData, 'id'>): Promise<ProductData> => {
const response = await api.post<ProductData>('/products', productData) const response = await api.post<ProductData>('/products', productData);
return response.data return response.data;
}, },
updateProduct: async (id: string, productData: Partial<ProductData>): Promise<ProductData> => { updateProduct: async (id: string, productData: Partial<ProductData>): Promise<ProductData> => {
const response = await api.put<ProductData>(`/products/${id}`, productData) const response = await api.put<ProductData>(`/products/${id}`, productData);
return response.data return response.data;
}, },
deleteProduct: async (id: string): Promise<void> => { deleteProduct: async (id: string): Promise<void> => {
await api.delete(`/products/${id}`) await api.delete(`/products/${id}`);
}, },
// Orders // Orders
getOrders: async (): Promise<OrderData[]> => { getOrders: async (): Promise<OrderData[]> => {
const response = await api.get<OrderData[]>('/orders') const response = await api.get<OrderData[]>('/orders');
return response.data return response.data;
}, },
getOrder: async (id: string): Promise<OrderData> => { getOrder: async (id: string): Promise<OrderData> => {
const response = await api.get<OrderData>(`/orders/${id}`) const response = await api.get<OrderData>(`/orders/${id}`);
return response.data return response.data;
}, },
createOrder: async (orderData: Omit<OrderData, 'id'>): Promise<OrderData> => { createOrder: async (orderData: Omit<OrderData, 'id'>): Promise<OrderData> => {
const response = await api.post<OrderData>('/orders', orderData) const response = await api.post<OrderData>('/orders', orderData);
return response.data return response.data;
}, },
} };
} }

View file

@ -7,7 +7,7 @@ import { ProductData } from '../types';
export function useProducts() { export function useProducts() {
const [products, setProducts] = useState<ProductData[]>([]); const [products, setProducts] = useState<ProductData[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { getProducts } = useApi(); const { getProducts } = useApi();
const { withLoader } = useLoader(); const { withLoader } = useLoader();
const { addNotification } = useToast(); const { addNotification } = useToast();
@ -15,28 +15,25 @@ export function useProducts() {
const fetchProducts = async () => { const fetchProducts = async () => {
try { try {
setError(null); setError(null);
// Use withLoader to show loading state and handle errors // Use withLoader to show loading state and handle errors
const data = await withLoader( const data = await withLoader(() => getProducts(), 'Loading products...');
() => getProducts(),
'Loading products...'
);
setProducts(data); setProducts(data);
// // Show success toast // // Show success toast
// addNotification({ // addNotification({
// type: 'success', // type: 'success',
// title: 'Products Loaded', // title: 'Products Loaded',
// message: `Successfully loaded ${data.length} products.`, // message: `Successfully loaded ${data.length} products.`,
// duration: 3000, // duration: 3000,
// }); // });
return data; return data;
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load products'; const errorMessage = err instanceof Error ? err.message : 'Failed to load products';
setError(errorMessage); setError(errorMessage);
// Show error toast // Show error toast
addNotification({ addNotification({
type: 'error', type: 'error',
@ -44,7 +41,7 @@ export function useProducts() {
message: errorMessage, message: errorMessage,
duration: 5000, duration: 5000,
}); });
return []; return [];
} }
}; };
@ -52,6 +49,7 @@ export function useProducts() {
// Optionally auto-fetch on mount // Optionally auto-fetch on mount
useEffect(() => { useEffect(() => {
fetchProducts(); fetchProducts();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return { return {
@ -60,4 +58,4 @@ export function useProducts() {
loading: false, // Loading is handled by the global loader loading: false, // Loading is handled by the global loader
refetch: fetchProducts, refetch: fetchProducts,
}; };
} }

View file

@ -1,9 +1,9 @@
import React from 'react' import React from 'react';
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom';
import { AppProvider } from './context/AppContext' import { AppProvider } from './context/AppContext';
import App from './App.tsx' import App from './App.tsx';
import './index.css' import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
@ -12,5 +12,5 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<App /> <App />
</AppProvider> </AppProvider>
</BrowserRouter> </BrowserRouter>
</React.StrictMode>, </React.StrictMode>
) );

View file

@ -29,6 +29,7 @@ export default function Cart() {
clearCart(); clearCart();
navigate('/orders'); navigate('/orders');
} catch (error) { } catch (error) {
console.error(error);
alert('Failed to create order. Please try again.'); alert('Failed to create order. Please try again.');
} }
}; };
@ -51,7 +52,7 @@ export default function Cart() {
return ( return (
<div> <div>
<h1 className="text-3xl font-bold text-white mb-8">Shopping Cart</h1> <h1 className="text-3xl font-bold text-white mb-8">Shopping Cart</h1>
<div className="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden"> <div className="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
{cart.map((item) => ( {cart.map((item) => (
<div <div
@ -65,12 +66,12 @@ export default function Cart() {
className="w-20 h-20 object-cover rounded" className="w-20 h-20 object-cover rounded"
/> />
)} )}
<div className="flex-1"> <div className="flex-1">
<h3 className="text-lg font-semibold text-white">{item.name}</h3> <h3 className="text-lg font-semibold text-white">{item.name}</h3>
<p className="text-blue-400 font-bold">${item.price}</p> <p className="text-blue-400 font-bold">${item.price}</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => updateCartQuantity(item.id, item.quantity - 1)} onClick={() => updateCartQuantity(item.id, item.quantity - 1)}
@ -86,11 +87,11 @@ export default function Cart() {
+ +
</button> </button>
</div> </div>
<p className="text-white font-bold min-w-[100px] text-right"> <p className="text-white font-bold min-w-[100px] text-right">
${(item.price * item.quantity).toFixed(2)} ${(item.price * item.quantity).toFixed(2)}
</p> </p>
<button <button
onClick={() => removeFromCart(item.id)} onClick={() => removeFromCart(item.id)}
className="text-red-400 hover:text-red-300 p-2 transition-colors" className="text-red-400 hover:text-red-300 p-2 transition-colors"
@ -107,7 +108,7 @@ export default function Cart() {
<span className="text-gray-400">Total:</span>{' '} <span className="text-gray-400">Total:</span>{' '}
<span className="text-white font-bold">${cartTotal.toFixed(2)}</span> <span className="text-white font-bold">${cartTotal.toFixed(2)}</span>
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
onClick={clearCart} onClick={clearCart}
@ -125,4 +126,4 @@ export default function Cart() {
</div> </div>
</div> </div>
); );
} }

View file

@ -1,15 +1,13 @@
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom';
import { ModalExample } from '../context/modals/ModalExample' import { ModalExample } from '../context/modals/ModalExample';
import { ToastExample } from '../context/toasts/ToastExample' import { ToastExample } from '../context/toasts/ToastExample';
import { LoaderExample } from '../context/loaders/LoaderExample' import { LoaderExample } from '../context/loaders/LoaderExample';
export function Home() { export function Home() {
return ( return (
<div className="space-y-12"> <div className="space-y-12">
<div className="text-center py-12"> <div className="text-center py-12">
<h1 className="text-5xl font-bold text-white mb-4"> <h1 className="text-5xl font-bold text-white mb-4">Welcome to Crafting Shop</h1>
Welcome to Crafting Shop
</h1>
<p className="text-xl text-gray-300 mb-8"> <p className="text-xl text-gray-300 mb-8">
Your one-stop shop for premium crafting supplies Your one-stop shop for premium crafting supplies
</p> </p>
@ -39,7 +37,8 @@ export function Home() {
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700"> <div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-2xl font-semibold text-white mb-4">Modal System Demo</h2> <h2 className="text-2xl font-semibold text-white mb-4">Modal System Demo</h2>
<p className="text-gray-400 mb-6"> <p className="text-gray-400 mb-6">
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.
</p> </p>
<ModalExample /> <ModalExample />
</div> </div>
@ -47,7 +46,8 @@ export function Home() {
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700"> <div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-2xl font-semibold text-white mb-4">Toast System Demo</h2> <h2 className="text-2xl font-semibold text-white mb-4">Toast System Demo</h2>
<p className="text-gray-400 mb-6"> <p className="text-gray-400 mb-6">
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.
</p> </p>
<ToastExample /> <ToastExample />
</div> </div>
@ -55,10 +55,11 @@ export function Home() {
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700"> <div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-2xl font-semibold text-white mb-4">Loader System Demo</h2> <h2 className="text-2xl font-semibold text-white mb-4">Loader System Demo</h2>
<p className="text-gray-400 mb-6"> <p className="text-gray-400 mb-6">
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.
</p> </p>
<LoaderExample /> <LoaderExample />
</div> </div>
</div> </div>
) );
} }

View file

@ -1,52 +1,52 @@
import { useState } from 'react' import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom' import { useNavigate, Link } from 'react-router-dom';
import { useApp } from '../context/AppContext' import { useApp } from '../context/AppContext';
import { useApi } from '../hooks/useApi' import { useApi } from '../hooks/useApi';
import { User } from '../types' import { User } from '../types';
export default function Login() { export default function Login() {
const [email, setEmail] = useState('') const [email, setEmail] = useState('');
const [password, setPassword] = useState('') const [password, setPassword] = useState('');
const [error, setError] = useState('') const [error, setError] = useState('');
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const navigate = useNavigate() const navigate = useNavigate();
const { login } = useApp() const { login } = useApp();
const { login: loginApi } = useApi() const { login: loginApi } = useApi();
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault();
setError('') setError('');
setLoading(true) setLoading(true);
try { try {
const response = await loginApi(email, password) const response = await loginApi(email, password);
// Convert UserData to User type // Convert UserData to User type
const user: User = { const user: User = {
id: parseInt(response.user.id), id: parseInt(response.user.id),
username: response.user.username, username: response.user.username,
email: response.user.email, email: response.user.email,
} };
login(user, response.token) login(user, response.token);
navigate('/') navigate('/');
} catch (err) { } 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 { } finally {
setLoading(false) setLoading(false);
} }
} };
return ( return (
<div className="max-w-md mx-auto"> <div className="max-w-md mx-auto">
<h1 className="text-3xl font-bold text-white mb-8 text-center">Login</h1> <h1 className="text-3xl font-bold text-white mb-8 text-center">Login</h1>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{error && ( {error && (
<div className="bg-red-900 border border-red-700 text-red-100 px-4 py-3 rounded"> <div className="bg-red-900 border border-red-700 text-red-100 px-4 py-3 rounded">
{error} {error}
</div> </div>
)} )}
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2"> <label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
Email Email
@ -60,7 +60,7 @@ export default function Login() {
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
<div> <div>
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2"> <label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2">
Password Password
@ -74,7 +74,7 @@ export default function Login() {
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
@ -83,13 +83,13 @@ export default function Login() {
{loading ? 'Logging in...' : 'Login'} {loading ? 'Logging in...' : 'Login'}
</button> </button>
</form> </form>
<p className="mt-6 text-center text-gray-400"> <p className="mt-6 text-center text-gray-400">
Don't have an account?{' '} Don&apos;t have an account?
<Link to="/register" className="text-blue-400 hover:text-blue-300"> <Link to="/register" className="text-blue-400 hover:text-blue-300">
Register Register
</Link> </Link>
</p> </p>
</div> </div>
) );
} }

View file

@ -1,34 +1,35 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom';
import { useApp } from '../context/AppContext' import { useApp } from '../context/AppContext';
import { useApi } from '../hooks/useApi' import { useApi } from '../hooks/useApi';
import { OrderData } from '../types' import { OrderData } from '../types';
export function Orders() { export function Orders() {
const [orders, setOrders] = useState<OrderData[]>([]) const [orders, setOrders] = useState<OrderData[]>([]);
const [loading, setLoading] = useState<boolean>(true) const [loading, setLoading] = useState<boolean>(true);
const navigate = useNavigate() const navigate = useNavigate();
const { user } = useApp() const { user } = useApp();
const { getOrders } = useApi() const { getOrders } = useApi();
useEffect(() => { useEffect(() => {
if (!user) { if (!user) {
navigate('/login') navigate('/login');
return return;
} }
fetchOrders() fetchOrders();
}, [user, navigate]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, navigate]);
const fetchOrders = async () => { const fetchOrders = async () => {
try { try {
const data = await getOrders() const data = await getOrders();
setOrders(data) setOrders(data);
} catch (error) { } catch (error) {
console.error('Error fetching orders:', error) console.error('Error fetching orders:', error);
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
const getStatusColor = (status: string): string => { const getStatusColor = (status: string): string => {
const colors: Record<string, string> = { const colors: Record<string, string> = {
@ -37,22 +38,22 @@ export function Orders() {
shipped: 'bg-purple-900 text-purple-200 border-purple-700', shipped: 'bg-purple-900 text-purple-200 border-purple-700',
delivered: 'bg-green-900 text-green-200 border-green-700', delivered: 'bg-green-900 text-green-200 border-green-700',
cancelled: 'bg-red-900 text-red-200 border-red-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) { if (loading) {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="text-gray-400">Loading orders...</div> <div className="text-gray-400">Loading orders...</div>
</div> </div>
) );
} }
return ( return (
<div> <div>
<h1 className="text-3xl font-bold text-white mb-8">My Orders</h1> <h1 className="text-3xl font-bold text-white mb-8">My Orders</h1>
{orders.length === 0 ? ( {orders.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-400 mb-8">You have no orders yet</p> <p className="text-gray-400 mb-8">You have no orders yet</p>
@ -72,9 +73,7 @@ export function Orders() {
> >
<div className="p-4 border-b border-gray-700 flex justify-between items-center"> <div className="p-4 border-b border-gray-700 flex justify-between items-center">
<div> <div>
<h3 className="text-lg font-semibold text-white"> <h3 className="text-lg font-semibold text-white">Order #{order.id}</h3>
Order #{order.id}
</h3>
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">
{new Date(order.created_at).toLocaleDateString()} {new Date(order.created_at).toLocaleDateString()}
</p> </p>
@ -87,7 +86,7 @@ export function Orders() {
{order.status.charAt(0).toUpperCase() + order.status.slice(1)} {order.status.charAt(0).toUpperCase() + order.status.slice(1)}
</span> </span>
</div> </div>
<div className="p-4"> <div className="p-4">
{order.items.map((item) => ( {order.items.map((item) => (
<div <div
@ -96,9 +95,7 @@ export function Orders() {
> >
<div> <div>
<p className="text-white font-medium">Product #{item.product_id}</p> <p className="text-white font-medium">Product #{item.product_id}</p>
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">Quantity: {item.quantity}</p>
Quantity: {item.quantity}
</p>
</div> </div>
<p className="text-white font-bold"> <p className="text-white font-bold">
${(item.price * item.quantity).toFixed(2)} ${(item.price * item.quantity).toFixed(2)}
@ -106,18 +103,14 @@ export function Orders() {
</div> </div>
))} ))}
</div> </div>
<div className="p-4 bg-gray-750 border-t border-gray-700 flex justify-between items-center"> <div className="p-4 bg-gray-750 border-t border-gray-700 flex justify-between items-center">
<div className="text-sm text-gray-400"> <div className="text-sm text-gray-400">
{order.shipping_address && ( {order.shipping_address && <span>Ship to: {order.shipping_address}</span>}
<span>Ship to: {order.shipping_address}</span>
)}
</div> </div>
<div className="text-xl"> <div className="text-xl">
<span className="text-gray-400">Total:</span>{' '} <span className="text-gray-400">Total:</span>{' '}
<span className="text-white font-bold"> <span className="text-white font-bold">${order.total_amount}</span>
${order.total_amount}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -125,5 +118,5 @@ export function Orders() {
</div> </div>
)} )}
</div> </div>
) );
} }

View file

@ -1,10 +1,10 @@
import { useApp } from '../context/AppContext' import { useApp } from '../context/AppContext';
import { useProducts } from '../hooks/useProducts' import { useProducts } from '../hooks/useProducts';
import { CartItem } from '../types' import { CartItem } from '../types';
export function Products() { export function Products() {
const { products, refetch } = useProducts() const { products, refetch } = useProducts();
const { addToCart } = useApp() const { addToCart } = useApp();
return ( return (
<div> <div>
@ -31,19 +31,11 @@ export function Products() {
/> />
)} )}
<div className="p-4"> <div className="p-4">
<h3 className="text-lg font-semibold text-white mb-2"> <h3 className="text-lg font-semibold text-white mb-2">{product.name}</h3>
{product.name} <p className="text-gray-400 text-sm mb-3 line-clamp-2">{product.description}</p>
</h3>
<p className="text-gray-400 text-sm mb-3 line-clamp-2">
{product.description}
</p>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xl font-bold text-blue-400"> <span className="text-xl font-bold text-blue-400">${product.price}</span>
${product.price} <span className="text-sm text-gray-400">Stock: {product.stock}</span>
</span>
<span className="text-sm text-gray-400">
Stock: {product.stock}
</span>
</div> </div>
<button <button
onClick={() => { onClick={() => {
@ -52,7 +44,7 @@ export function Products() {
name: product.name, name: product.name,
price: product.price, price: product.price,
quantity: 1, quantity: 1,
image_url: product.image_url image_url: product.image_url,
}; };
addToCart(cartItem); addToCart(cartItem);
}} }}
@ -70,5 +62,5 @@ export function Products() {
</div> </div>
)} )}
</div> </div>
) );
} }

View file

@ -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 (
<div className="max-w-md mx-auto">
<h1 className="text-3xl font-bold text-white mb-8 text-center">Register</h1>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-900 border border-red-700 text-red-100 px-4 py-3 rounded">
{error}
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="first_name" className="block text-sm font-medium text-gray-300 mb-2">
First Name
</label>
<input
id="first_name"
name="first_name"
type="text"
value={formData.first_name}
onChange={handleChange}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="last_name" className="block text-sm font-medium text-gray-300 mb-2">
Last Name
</label>
<input
id="last_name"
name="last_name"
type="text"
value={formData.last_name}
onChange={handleChange}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-300 mb-2">
Username
</label>
<input
id="username"
name="username"
type="text"
value={formData.username}
onChange={handleChange}
required
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
Email
</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2">
Password
</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
required
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-300 mb-2">
Confirm Password
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
required
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Registering...' : 'Register'}
</button>
</form>
<p className="mt-6 text-center text-gray-400">
Already have an account?{' '}
<Link to="/login" className="text-blue-400 hover:text-blue-300">
Login
</Link>
</p>
</div>
)
}

View file

@ -1,14 +1,14 @@
import { useState, FormEvent, ChangeEvent } from 'react' import { useState, FormEvent, ChangeEvent } from 'react';
import { useNavigate, Link } from 'react-router-dom' import { useNavigate, Link } from 'react-router-dom';
import { useApi } from '../hooks/useApi' import { useApi } from '../hooks/useApi';
interface FormData { interface FormData {
email: string email: string;
username: string username: string;
password: string password: string;
confirmPassword: string confirmPassword: string;
first_name: string first_name: string;
last_name: string last_name: string;
} }
export function Register() { export function Register() {
@ -19,35 +19,35 @@ export function Register() {
confirmPassword: '', confirmPassword: '',
first_name: '', first_name: '',
last_name: '', last_name: '',
}) });
const [error, setError] = useState<string>('') const [error, setError] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false);
const navigate = useNavigate() const navigate = useNavigate();
const { register } = useApi() const { register } = useApi();
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFormData({ setFormData({
...formData, ...formData,
[e.target.name]: e.target.value, [e.target.name]: e.target.value,
}) });
} };
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault();
setError('') setError('');
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match') setError('Passwords do not match');
return return;
} }
if (formData.password.length < 6) { if (formData.password.length < 6) {
setError('Password must be at least 6 characters') setError('Password must be at least 6 characters');
return return;
} }
setLoading(true) setLoading(true);
try { try {
await register({ await register({
@ -56,26 +56,26 @@ export function Register() {
password: formData.password, password: formData.password,
first_name: formData.first_name, first_name: formData.first_name,
last_name: formData.last_name, last_name: formData.last_name,
}) });
navigate('/login') navigate('/login');
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.error || 'Registration failed. Please try again.') setError(err.response?.data?.error || 'Registration failed. Please try again.');
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
return ( return (
<div className="max-w-md mx-auto"> <div className="max-w-md mx-auto">
<h1 className="text-3xl font-bold text-white mb-8 text-center">Register</h1> <h1 className="text-3xl font-bold text-white mb-8 text-center">Register</h1>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{error && ( {error && (
<div className="bg-red-900 border border-red-700 text-red-100 px-4 py-3 rounded"> <div className="bg-red-900 border border-red-700 text-red-100 px-4 py-3 rounded">
{error} {error}
</div> </div>
)} )}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label htmlFor="first_name" className="block text-sm font-medium text-gray-300 mb-2"> <label htmlFor="first_name" className="block text-sm font-medium text-gray-300 mb-2">
@ -90,7 +90,7 @@ export function Register() {
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
<div> <div>
<label htmlFor="last_name" className="block text-sm font-medium text-gray-300 mb-2"> <label htmlFor="last_name" className="block text-sm font-medium text-gray-300 mb-2">
Last Name Last Name
@ -105,7 +105,7 @@ export function Register() {
/> />
</div> </div>
</div> </div>
<div> <div>
<label htmlFor="username" className="block text-sm font-medium text-gray-300 mb-2"> <label htmlFor="username" className="block text-sm font-medium text-gray-300 mb-2">
Username Username
@ -120,7 +120,7 @@ export function Register() {
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2"> <label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
Email Email
@ -135,7 +135,7 @@ export function Register() {
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
<div> <div>
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2"> <label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2">
Password Password
@ -150,7 +150,7 @@ export function Register() {
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
<div> <div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-300 mb-2"> <label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-300 mb-2">
Confirm Password Confirm Password
@ -165,7 +165,7 @@ export function Register() {
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
@ -174,7 +174,7 @@ export function Register() {
{loading ? 'Registering...' : 'Register'} {loading ? 'Registering...' : 'Register'}
</button> </button>
</form> </form>
<p className="mt-6 text-center text-gray-400"> <p className="mt-6 text-center text-gray-400">
Already have an account?{' '} Already have an account?{' '}
<Link to="/login" className="text-blue-400 hover:text-blue-300"> <Link to="/login" className="text-blue-400 hover:text-blue-300">
@ -182,5 +182,5 @@ export function Register() {
</Link> </Link>
</p> </p>
</div> </div>
) );
} }

View file

@ -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',
}
)
)

View file

@ -1,4 +1,4 @@
export interface ApiResponse<T> { export interface ApiResponse<T> {
data: T; data: T;
message?: string; message?: string;
} }

View file

@ -1,3 +1,3 @@
export interface ModalContentProps { export interface ModalContentProps {
onClose: () => void; onClose: () => void;
} }

View file

@ -21,4 +21,4 @@ export interface Order {
total_amount: number; total_amount: number;
shipping_address: string; shipping_address: string;
items: OrderItem[]; items: OrderItem[];
} }

View file

@ -23,4 +23,4 @@ export interface CartItem {
quantity: number; quantity: number;
image_url?: string; image_url?: string;
[key: string]: any; [key: string]: any;
} }

View file

@ -27,4 +27,4 @@ export interface RegisterData {
export interface AuthResponse { export interface AuthResponse {
token: string; token: string;
user: UserData; user: UserData;
} }