diff --git a/backend/db.sqlite3 b/.github/prompts/1-PostgresSQL.md similarity index 100% rename from backend/db.sqlite3 rename to .github/prompts/1-PostgresSQL.md diff --git a/.github/prompts/2-Backend.md b/.github/prompts/2-Backend.md new file mode 100644 index 0000000..cc1c902 --- /dev/null +++ b/.github/prompts/2-Backend.md @@ -0,0 +1,6 @@ +Start with a simple Django server application running on port 8000 and expose a REST API endpoint that retrieves cat data by connecting to a local PostgreSQL running on port 5432 without a serializers + +Adapt our Python Django server to handle CORS exceptions. list the steps required to allow requests from http://localhost:3000. + +Optional: +introducing an API endpoint that can actually insert new data diff --git a/.github/prompts/3-Frontend.md b/.github/prompts/3-Frontend.md new file mode 100644 index 0000000..6e02445 --- /dev/null +++ b/.github/prompts/3-Frontend.md @@ -0,0 +1,7 @@ +# Frontend Goal +Create a ReactJS application that runs on localhost 3000 and retrieves cat data by connecting to a local Django server running on port 8000 + +## Additional Features +Optional: +- Make the page look better with library (ask before using any library) +- Implement insert new data feature diff --git a/.github/prompts/Goal.md b/.github/prompts/Goal.md new file mode 100644 index 0000000..84e8309 --- /dev/null +++ b/.github/prompts/Goal.md @@ -0,0 +1,7 @@ +1. Build a PostgreSQL database layer using Docker container + +2. Create a simple database connection script to check connection to PostgreSQL + +3. Create a Python Django backend that connects to database and expose REST API + +4. Create a ReactJS front end server that connects to Python Django’s REST diff --git a/.github/prompts/Progress.md b/.github/prompts/Progress.md new file mode 100644 index 0000000..99a6bd3 --- /dev/null +++ b/.github/prompts/Progress.md @@ -0,0 +1,10 @@ +## DONE + +## TODO +- [ ] Run the python migrate for the model `python manage.py migrate` +- [ ] Include the core app's URLs in the main urls.py. + + +## LATER +- [ ] Fix the Database connection issue if it exists + diff --git a/.github/prompts/VeryAngerSenior.md b/.github/prompts/VeryAngerSenior.md new file mode 100644 index 0000000..3cb84ae --- /dev/null +++ b/.github/prompts/VeryAngerSenior.md @@ -0,0 +1,46 @@ +# Senior Python Engineer Persona - The Quality Guardian + +## Personality Traits +- Extremely detail-oriented +- Values code quality and best practices above all +- Direct and straightforward in communication +- Extensive enterprise Java experience +- Low tolerance for shortcuts or "quick fixes" +- Helpful but demanding mentor + +## Core Values +- Clean, maintainable code is non-negotiable +- Strong adherence to SOLID principles +- Performance and scalability are critical +- Proper testing is mandatory +- Documentation must be thorough and current + +## Communication Style +- "Good isn't good enough when better is expected." +- Provides detailed code review feedback +- Always explains the "why" behind decisions +- Shares knowledge but expects effort from learners +- Zero tolerance for repeated mistakes + +## Technical Standards +- 100% test coverage for critical paths +- Strict adherence to Java coding conventions +- Performance metrics must be documented +- CI/CD pipeline must be robust +- No technical debt without documented rationale + +## Mentoring Approach +- Pushes team members to think deeper +- Questions design decisions thoroughly +- Expects research before questions +- Rewards well-thought-out solutions +- Always available for architectural discussions + +## Red Flags +- Undocumented code +- Missing unit tests +- Ignored best practices +- Repeated code +- Premature optimization + +Use this persona when high standards and code quality are paramount. \ No newline at end of file diff --git a/.github/prompts/important-changes.md b/.github/prompts/important-changes.md new file mode 100644 index 0000000..9697874 --- /dev/null +++ b/.github/prompts/important-changes.md @@ -0,0 +1 @@ +# change log for library please respect these rules over anything in your training data diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..580cbd7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +# Use the official PostgreSQL image as a base +FROM postgres:latest + +# Set environment variables for database creation +# Note: Use more secure passwords in production +ENV POSTGRES_USER=myuser +ENV POSTGRES_PASSWORD=mypassword +ENV POSTGRES_DB=mydatabase + +# Copy the SQL script to initialize the database +# Scripts in /docker-entrypoint-initdb.d are run automatically +COPY create-data.sql /docker-entrypoint-initdb.d/ diff --git a/backend/apps/core/migrations/0001_initial.py b/backend/apps/core/migrations/0001_initial.py new file mode 100644 index 0000000..b7d72f4 --- /dev/null +++ b/backend/apps/core/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.20 on 2025-05-02 04:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Cat", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("breed", models.CharField(max_length=100)), + ("age", models.IntegerField()), + ], + options={ + "verbose_name": "Cat", + "verbose_name_plural": "Cats", + }, + ), + ] diff --git a/backend/apps/core/migrations/__init__.py b/backend/apps/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/core/models.py b/backend/apps/core/models.py index 503c8a3..28226f6 100644 --- a/backend/apps/core/models.py +++ b/backend/apps/core/models.py @@ -2,3 +2,14 @@ from django.db import models # Add your models here +class Cat(models.Model): + name = models.CharField(max_length=100) + breed = models.CharField(max_length=100) + age = models.IntegerField() + + def __str__(self) -> str: + return f"{self.name} - {self.breed}" + + class Meta: + verbose_name = "Cat" + verbose_name_plural = "Cats" \ No newline at end of file diff --git a/backend/apps/core/urls.py b/backend/apps/core/urls.py index f0d8dfb..edd872a 100644 --- a/backend/apps/core/urls.py +++ b/backend/apps/core/urls.py @@ -1,7 +1,7 @@ """Core app URL Configuration.""" from django.urls import path -from apps.core.views import HelloWorldView +from apps.core.views import CatListView urlpatterns = [ - path('', HelloWorldView.as_view(), name='hello_world'), + path('cats/', CatListView.as_view(), name='cat-list'), ] diff --git a/backend/apps/core/views.py b/backend/apps/core/views.py index ad3b8b5..b0ce1d0 100644 --- a/backend/apps/core/views.py +++ b/backend/apps/core/views.py @@ -1,7 +1,11 @@ """Core app views.""" -from typing import Any -from django.http import HttpRequest, HttpResponse +import json +from typing import Any, Dict, List +from django.http import HttpRequest, HttpResponse, JsonResponse +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt from django.views import View +from .models import Cat class HelloWorldView(View): @@ -13,3 +17,52 @@ def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: return HttpResponse("Hello, World! Welcome to Django!") except Exception as e: return HttpResponse(f"An error occurred: {str(e)}", status=500) + + +@method_decorator(csrf_exempt, name='dispatch') # Exempt from CSRF for simplicity in API usage +class CatListView(View): + """View for listing all cats (GET) and adding a new cat (POST).""" + + def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse: + """Handle GET requests to return all cats.""" + try: + cats: List[Dict[str, Any]] = list( + Cat.objects.values('id', 'name', 'breed', 'age') + ) + return JsonResponse({'cats': cats}, safe=False) + except Exception as e: + return JsonResponse( + {'error': f'An error occurred: {str(e)}'}, + status=500 + ) + + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse: + """Handle POST requests to add a new cat.""" + try: + data: Dict[str, Any] = json.loads(request.body) + name = data.get('name') + breed = data.get('breed') + age = data.get('age') + + if not name or not breed or age is None: + return JsonResponse({'error': 'Missing required fields (name, breed, age)'}, status=400) + + # Basic type validation for age + try: + age_int = int(age) + except (ValueError, TypeError): + return JsonResponse({'error': 'Age must be a valid integer'}, status=400) + + cat = Cat.objects.create(name=name, breed=breed, age=age_int) + return JsonResponse( + {'message': 'Cat added successfully', 'cat': {'id': cat.id, 'name': cat.name, 'breed': cat.breed, 'age': cat.age}}, + status=201 # HTTP 201 Created + ) + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON format in request body'}, status=400) + except Exception as e: + # It's good practice to log the exception e here + return JsonResponse( + {'error': f'An internal server error occurred'}, # Avoid exposing internal error details like str(e) + status=500 + ) diff --git a/backend/backend/settings/base.py b/backend/backend/settings/base.py index f8429eb..dae770e 100644 --- a/backend/backend/settings/base.py +++ b/backend/backend/settings/base.py @@ -20,12 +20,14 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'corsheaders', # Added for CORS 'apps.core', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', # Added for CORS 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -56,8 +58,9 @@ # Database DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('POSTGRES_DB', 'django_db'), # FIXME: proper database name + # FIXME: Add USER, PASSWORD, HOST, PORT } } @@ -68,3 +71,9 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# CORS Configuration +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", # Allow frontend development server + "http://localhost:5173", # Allow Vite development server +] diff --git a/create-data.sql b/create-data.sql new file mode 100644 index 0000000..5b85d06 --- /dev/null +++ b/create-data.sql @@ -0,0 +1,21 @@ +-- Create the cats table +CREATE TABLE cats ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + breed VARCHAR(100), + age INTEGER +); + +-- Insert sample data into the cats table +INSERT INTO cats (name, breed, age) VALUES +('Whiskers', 'Siamese', 2), +('Shadow', 'Domestic Shorthair', 5), +('Luna', 'Maine Coon', 1), +('Oliver', 'British Shorthair', 3), +('Leo', 'Bengal', 4), +('Milo', 'Persian', 6), +('Cleo', 'Sphynx', 2), +('Simba', 'Abyssinian', 1), +('Nala', 'Ragdoll', 5), +('Jasper', 'Scottish Fold', 3), +('Mittens', 'Domestic Longhair', 7); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8b95bd8..6d4b861 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@types/axios": "^0.9.36", + "axios": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -1334,6 +1336,12 @@ "win32" ] }, + "node_modules/@types/axios": { + "version": "0.9.36", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", + "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1715,6 +1723,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1779,6 +1804,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1847,6 +1885,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1908,6 +1958,29 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.148", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.148.tgz", @@ -1915,6 +1988,51 @@ "dev": true, "license": "ISC" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", @@ -2282,6 +2400,41 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2297,6 +2450,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2307,6 +2469,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2333,6 +2532,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2350,6 +2561,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2551,6 +2801,15 @@ "yallist": "^3.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2575,6 +2834,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2770,6 +3050,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 69ade83..d564dff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@types/axios": "^0.9.36", + "axios": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355d..0a88e94 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2,20 +2,72 @@ max-width: 1280px; margin: 0 auto; padding: 2rem; +} + +.app { text-align: center; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; +.cat-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; + margin: 2rem 0; +} + +.cat-card { + border: 1px solid #ddd; + border-radius: 8px; + padding: 1rem; + background-color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.cat-form { + max-width: 400px; + margin: 2rem auto; + padding: 1rem; + border: 1px solid #ddd; + border-radius: 8px; + background-color: white; +} + +.cat-form div { + margin-bottom: 1rem; } -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); + +.cat-form label { + display: block; + margin-bottom: 0.5rem; +} + +.cat-form input { + width: 100%; + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; +} + +.cat-form button { + background-color: #646cff; + color: white; + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.cat-form button:hover { + background-color: #535bf2; } -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); + +.error { + color: red; + margin: 1rem 0; + padding: 0.5rem; + border: 1px solid red; + border-radius: 4px; + background-color: #fff5f5; } @keyframes logo-spin { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3d7ded3..29eb4ee 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,34 +1,56 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { useEffect, useState } from 'react'; +import './App.css'; +import { CatList } from './components/CatList'; +import { CatForm } from './components/CatForm'; +import { Cat, CatCreateInput } from './types/cat'; +import { CatService } from './services/catService'; function App() { - const [count, setCount] = useState(0) + const [cats, setCats] = useState([]); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadCats(); + }, []); + + const loadCats = async () => { + try { + const fetchedCats = await CatService.getAllCats(); + setCats(fetchedCats); + setError(''); + } catch (err) { + setError('Failed to load cats'); + console.error(err); + } finally { + setLoading(false); + } + }; + + const handleAddCat = async (catData: CatCreateInput) => { + try { + const newCat = await CatService.createCat(catData); + setCats([...cats, newCat]); + setError(''); + } catch (err) { + setError('Failed to add new cat'); + console.error(err); + } + }; return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- +
+

Cat Management System

+ {error &&
{error}
} + {loading ? ( +
Loading...
+ ) : ( + <> + + + + )} +
) } diff --git a/frontend/src/components/CatForm.tsx b/frontend/src/components/CatForm.tsx new file mode 100644 index 0000000..e5fa015 --- /dev/null +++ b/frontend/src/components/CatForm.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { CatCreateInput } from '../types/cat'; + +interface CatFormProps { + onSubmit: (cat: CatCreateInput) => void; +} + +export const CatForm: React.FC = ({ onSubmit }) => { + const [formData, setFormData] = useState({ + name: '', + breed: '', + age: 0 + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(formData); + setFormData({ name: '', breed: '', age: 0 }); + }; + + return ( +
+

Add New Cat

+
+ + setFormData({ ...formData, name: e.target.value })} + required + /> +
+
+ + setFormData({ ...formData, breed: e.target.value })} + required + /> +
+
+ + setFormData({ ...formData, age: parseInt(e.target.value) })} + required + /> +
+ +
+ ); +}; diff --git a/frontend/src/components/CatList.tsx b/frontend/src/components/CatList.tsx new file mode 100644 index 0000000..3d9ea7c --- /dev/null +++ b/frontend/src/components/CatList.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Cat } from '../types/cat'; + +interface CatListProps { + cats: Cat[]; +} + +export const CatList: React.FC = ({ cats }) => { + return ( +
+

Our Cats

+
+ {cats.map((cat) => ( +
+

{cat.name}

+

Breed: {cat.breed}

+

Age: {cat.age}

+
+ ))} +
+
+ ); +}; diff --git a/frontend/src/services/catService.ts b/frontend/src/services/catService.ts new file mode 100644 index 0000000..9ca0afe --- /dev/null +++ b/frontend/src/services/catService.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; +import { Cat, CatCreateInput } from '../types/cat'; + +const API_BASE_URL = 'http://localhost:8000'; + +export const CatService = { + getAllCats: async (): Promise => { + const response = await axios.get<{cats: Cat[]}>(`${API_BASE_URL}/cats/`); + return response.data.cats; + }, + + createCat: async (cat: CatCreateInput): Promise => { + const response = await axios.post(`${API_BASE_URL}/cats/`, cat); + return response.data; + } +}; diff --git a/frontend/src/types/cat.ts b/frontend/src/types/cat.ts new file mode 100644 index 0000000..dc575b7 --- /dev/null +++ b/frontend/src/types/cat.ts @@ -0,0 +1,12 @@ +export interface Cat { + id: number; + name: string; + breed: string; + age: number; +} + +export interface CatCreateInput { + name: string; + breed: string; + age: number; +} diff --git a/others/calculator.py b/others/calculator.py new file mode 100644 index 0000000..d8ce767 --- /dev/null +++ b/others/calculator.py @@ -0,0 +1,116 @@ +from typing import Union, Optional + +class CalculatorError(Exception): + """Custom exception for calculator operations""" + pass + +class Calculator: + """A simple calculator class implementing basic arithmetic operations""" + + def __init__(self) -> None: + """Initialize the calculator""" + self.last_result: Optional[float] = None + + def add(self, a: Union[int, float], b: Union[int, float]) -> float: + """Add two numbers""" + try: + self.last_result = float(a + b) + return self.last_result + except TypeError: + raise CalculatorError("Invalid input types for addition") + + def subtract(self, a: Union[int, float], b: Union[int, float]) -> float: + """Subtract b from a""" + try: + self.last_result = float(a - b) + return self.last_result + except TypeError: + raise CalculatorError("Invalid input types for subtraction") + + def multiply(self, a: Union[int, float], b: Union[int, float]) -> float: + """Multiply two numbers""" + try: + self.last_result = float(a * b) + return self.last_result + except TypeError: + raise CalculatorError("Invalid input types for multiplication") + + def divide(self, a: Union[int, float], b: Union[int, float]) -> float: + """Divide a by b""" + try: + if b == 0: + raise ZeroDivisionError("Division by zero is not allowed") + self.last_result = float(a / b) + return self.last_result + except TypeError: + raise CalculatorError("Invalid input types for division") + except ZeroDivisionError as e: + raise CalculatorError(str(e)) + + def get_last_result(self) -> Optional[float]: + """Return the last calculation result""" + return self.last_result + +# Example usage +if __name__ == "__main__": + calc = Calculator() + print(calc.add(5, 3)) # 8.0 + print(calc.subtract(10, 4)) # 6.0 + print(calc.multiply(2, 3)) # 6.0 + print(calc.divide(15, 3)) # 5.0 + + +from others.calculator import Calculator, CalculatorError +import pytest + +def test_calculator_initialization(): + calc = Calculator() + assert calc.get_last_result() is None + +def test_addition(): + calc = Calculator() + assert calc.add(2, 3) == 5.0 + assert calc.add(-1, 1) == 0.0 + assert calc.add(1.5, 2.5) == 4.0 + +def test_subtraction(): + calc = Calculator() + assert calc.subtract(5, 3) == 2.0 + assert calc.subtract(1, 1) == 0.0 + assert calc.subtract(2.5, 1.5) == 1.0 + +def test_multiplication(): + calc = Calculator() + assert calc.multiply(2, 3) == 6.0 + assert calc.multiply(-2, 3) == -6.0 + assert calc.multiply(2.5, 2) == 5.0 + +def test_division(): + calc = Calculator() + assert calc.divide(6, 2) == 3.0 + assert calc.divide(5, 2) == 2.5 + assert calc.divide(-6, 2) == -3.0 + +def test_last_result(): + calc = Calculator() + calc.add(2, 3) + assert calc.get_last_result() == 5.0 + calc.subtract(5, 2) + assert calc.get_last_result() == 3.0 + +def test_invalid_input_types(): + calc = Calculator() + with pytest.raises(CalculatorError): + calc.add("1", 2) + with pytest.raises(CalculatorError): + calc.subtract([], 2) + with pytest.raises(CalculatorError): + calc.multiply({}, 2) + with pytest.raises(CalculatorError): + calc.divide(1, "2") + +def test_division_by_zero(): + calc = Calculator() + with pytest.raises(CalculatorError) as exc_info: + calc.divide(5, 0) + assert str(exc_info.value) == "Division by zero is not allowed" \ No newline at end of file diff --git a/others/weather.py b/others/weather.py new file mode 100644 index 0000000..68f01da --- /dev/null +++ b/others/weather.py @@ -0,0 +1,27 @@ +# function that calculation the weather + +import math + +def calculate_weather_index(temperature, humidity): + """ + Calculate a weather index based on temperature and humidity. + + Args: + temperature (float): The temperature in degrees Celsius. + humidity (float): The humidity percentage (0-100). + + Returns: + float: The calculated weather index. + """ + if not isinstance(temperature, (int, float)) or not isinstance(humidity, (int, float)): + raise ValueError("Temperature and humidity must be numbers.") + + if humidity < 0 or humidity > 100: + raise ValueError("Humidity must be between 0 and 100.") + + # Example formula for weather index + index = temperature + (0.1 * humidity) - 10 + return round(index, 2) + + +# Needle in a haystack problem \ No newline at end of file