From ee030b70bca7ed966adbb2eae3359f55a9d0db86 Mon Sep 17 00:00:00 2001 From: Laxmi Khilnani Date: Tue, 14 Oct 2025 19:51:35 +0530 Subject: [PATCH] Initial commit for resumeformatter project --- .env.example | 2 + .gitea/workflows/docker-build-push.yml | 87 +++++++++++ .gitignore | 76 ++++++++++ README.md | 130 +++++++++++++++++ backend/Dockerfile | 85 +++++++++++ backend/README.md | 80 ++++++++++ backend/app/__init__.py | 0 backend/app/api/__init__.py | 0 backend/app/api/api.py | 6 + backend/app/api/endpoints/__init__.py | 0 backend/app/api/endpoints/people.py | 35 +++++ backend/app/core/config.py | 25 ++++ backend/app/crud/__init__.py | 1 + backend/app/crud/person.py | 27 ++++ backend/app/db/base.py | 3 + backend/app/db/base_class.py | 13 ++ backend/app/db/session.py | 19 +++ backend/app/main.py | 195 +++++++++++++++++++++++++ backend/app/models/__init__.py | 0 backend/app/models/person.py | 12 ++ backend/app/schemas/__init__.py | 0 backend/app/schemas/person.py | 34 +++++ backend/main.py | 8 + backend/requirements.txt | 7 + backend/test_api.py | 51 +++++++ docker-compose.yml | 28 ++++ frontend/.gitignore | 24 +++ frontend/App.tsx | 105 +++++++++++++ frontend/README.md | 20 +++ frontend/api_spec.md | 110 ++++++++++++++ frontend/components/AddPersonForm.tsx | 104 +++++++++++++ frontend/components/Icons.tsx | 27 ++++ frontend/components/Modal.tsx | 62 ++++++++ frontend/components/PersonList.tsx | 57 ++++++++ frontend/index.html | 24 +++ frontend/index.tsx | 16 ++ frontend/metadata.json | 5 + frontend/package.json | 21 +++ frontend/services/apiService.ts | 52 +++++++ frontend/tsconfig.json | 29 ++++ frontend/types.ts | 9 ++ frontend/vite.config.ts | 31 ++++ new_project.sh | 48 ++++++ 43 files changed, 1668 insertions(+) create mode 100644 .env.example create mode 100644 .gitea/workflows/docker-build-push.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/Dockerfile create mode 100644 backend/README.md create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/api.py create mode 100644 backend/app/api/endpoints/__init__.py create mode 100644 backend/app/api/endpoints/people.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/crud/__init__.py create mode 100644 backend/app/crud/person.py create mode 100644 backend/app/db/base.py create mode 100644 backend/app/db/base_class.py create mode 100644 backend/app/db/session.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/person.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/person.py create mode 100644 backend/main.py create mode 100644 backend/requirements.txt create mode 100644 backend/test_api.py create mode 100644 docker-compose.yml create mode 100644 frontend/.gitignore create mode 100644 frontend/App.tsx create mode 100644 frontend/README.md create mode 100644 frontend/api_spec.md create mode 100644 frontend/components/AddPersonForm.tsx create mode 100644 frontend/components/Icons.tsx create mode 100644 frontend/components/Modal.tsx create mode 100644 frontend/components/PersonList.tsx create mode 100644 frontend/index.html create mode 100644 frontend/index.tsx create mode 100644 frontend/metadata.json create mode 100644 frontend/package.json create mode 100644 frontend/services/apiService.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/types.ts create mode 100644 frontend/vite.config.ts create mode 100755 new_project.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..67f0f3e --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# Application name used for routing and Docker image name +APP_NAME=NEW_APP_NAME diff --git a/.gitea/workflows/docker-build-push.yml b/.gitea/workflows/docker-build-push.yml new file mode 100644 index 0000000..4e3da49 --- /dev/null +++ b/.gitea/workflows/docker-build-push.yml @@ -0,0 +1,87 @@ +name: Profile Linker Docker Build + +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + +env: + APP_NAME: NEW_APP_NAME + REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }} + +jobs: + deploy: + runs-on: ubuntu-latest + env: + BRANCH_NAME: ${{ gitea.ref_name }} + + name: Build and push Docker image + + steps: + - name: Install required packages + run: | + if command -v apk >/dev/null 2>&1; then + apk add --no-cache nodejs npm git docker + elif command -v apt-get >/dev/null 2>&1; then + apt-get update && apt-get install -y nodejs npm git docker.io + fi + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set deployment variables + id: vars + run: | + BRANCH="$BRANCH_NAME" + if [[ "$BRANCH" == "main" ]]; then + tag="latest" + elif [[ "$BRANCH" == "dev" ]]; then + tag="dev" + else + tag="test" + fi + + # Extract APP_NAME from .env file if it exists + if [ -f .env ]; then + APP_NAME=$(grep APP_NAME .env | cut -d '=' -f2) + fi + + echo "tag=$tag" >> $GITEA_OUTPUT + echo "app_name=$APP_NAME" >> $GITEA_OUTPUT + + # Login to Docker Hub before building to allow pulling base images + - name: Login to Docker Hub + run: | + echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USERNAME" --password-stdin + + # Pull base images explicitly before building + - name: Pull base images + run: | + docker pull node:20-alpine + docker pull python:3.11-alpine + + - name: Build Docker image + run: | + APP_NAME="${{ steps.vars.outputs.app_name }}" + TAG="${{ steps.vars.outputs.tag }}" + + # Build with no-cache to avoid authentication issues + docker build --no-cache -t docker.io/$REGISTRY_USERNAME/$APP_NAME:$TAG \ + -t docker.io/$REGISTRY_USERNAME/$APP_NAME:${{ gitea.sha }} \ + -f ./backend/Dockerfile \ + . + + - name: Push Docker image + if: gitea.event_name != 'pull_request' + run: | + APP_NAME="${{ steps.vars.outputs.app_name }}" + TAG="${{ steps.vars.outputs.tag }}" + + docker push docker.io/$REGISTRY_USERNAME/$APP_NAME:$TAG + docker push docker.io/$REGISTRY_USERNAME/$APP_NAME:${{ gitea.sha }} + + echo "=== 🏗️ Build Summary ===" + echo "📦 Image Built: docker.io/$REGISTRY_USERNAME/$APP_NAME:${{ gitea.sha }}" + echo "=====================" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f96d61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,76 @@ +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.nox/ +.hypothesis/ +.venv +venv/ +ENV/ + +# Node.js / Frontend +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log +.pnpm-debug.log +.env.local +.env.development.local +.env.test.local +.env.production.local +.cache/ +coverage/ + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace +*.sublime-project + +# Docker +.docker/ + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0182f4 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# Profile Linker Application + +This project consists of a React frontend and a FastAPI backend for managing a list of people's profiles. + +## Getting Started with a New Project + +This repository serves as a template for creating new projects. To create a new project: + +1. Clone this repository +2. Run the initialization script with your desired app name: + +```bash +./new_project.sh YourAppName +``` + +The script will: +- Replace all references to "ResumeFormatter" with your app name +- Delete the Git history and initialize a new Git repository +- Create a .env file with your app name +- Rename the project folder to your app name (lowercase) +- Delete itself after completion + +## Project Structure + +``` +/ +├── frontend/ # React frontend +│ ├── components/ # React components +│ ├── services/ # API services +│ └── ... # Other frontend files +├── backend/ # FastAPI backend +│ ├── app/ # FastAPI application +│ │ ├── api/ # API endpoints +│ │ ├── core/ # Core application components +│ │ ├── crud/ # CRUD operations +│ │ ├── db/ # Database components +│ │ ├── models/ # Database models +│ │ ├── schemas/ # Pydantic schemas +│ │ └── main.py # FastAPI application +│ ├── main.py # Entry point +│ └── requirements.txt # Python dependencies +├── .gitea/ # Gitea configuration +│ └── workflows/ # Gitea Actions workflows +├── docker-compose.yml # Docker Compose configuration +└── README.md # This file +``` + +## Environment Variables + +The application uses environment variables for configuration. Create a `.env` file in the root directory with the following variables: + +``` +APP_NAME=ResumeFormatter +``` + +You can also use the provided `.env.example` file as a template. + +## URL Structure + +The application follows a specific URL structure based on the APP_NAME environment variable: + +- Frontend: `http://localhost:8080/{APP_NAME}` +- API: `http://localhost:8080/{APP_NAME}/api` +- API Documentation: `http://localhost:8080/{APP_NAME}/api/docs` +- Static Assets: `http://localhost:8080/{APP_NAME}/assets` + +This structure allows for multiple applications to be hosted under the same domain with different paths. + +## Running the Application with Docker + +1. Build and start the containers: + +```bash +docker-compose up --build +``` + +2. Access the application at http://localhost:8080/ResumeFormatter +3. Access the API at http://localhost:8080/ResumeFormatter/api +4. Access the API documentation at http://localhost:8080/ResumeFormatter/api/docs + +## API Endpoints + +The API provides the following endpoints: + +- `GET /ResumeFormatter/api/people`: Get all people +- `POST /ResumeFormatter/api/people`: Create a new person + +## Frontend Integration + +The backend serves the frontend as static assets. The frontend is configured to use the APP_NAME as the base path for all assets and API calls. Key integration points: + +1. **Vite Configuration**: The frontend uses Vite with a base path set to `/{APP_NAME}/` in `vite.config.ts` +2. **API Service**: The frontend API service uses `/{APP_NAME}/api` as the base URL for all API calls +3. **Static Assets**: All static assets are served under the `/{APP_NAME}/assets` path + +## Database + +The application uses SQLite for data storage. The database file is created at `./ResumeFormatter.db` in the root directory. + +## Continuous Integration + +This project includes a Gitea Actions workflow for building and pushing Docker images to Docker Hub. The workflow is triggered on pushes and pull requests to the `dev` branch. + +### Gitea Actions Workflow + +The workflow performs the following steps: + +1. Installs required packages (Node.js, Git, Docker) +2. Checks out the code +3. Sets deployment variables based on branch name and .env file +4. Logs in to Docker Hub using the provided variables and secrets +5. Builds and pushes the Docker image with tags: + - `latest` (for main branch) or `dev` (for dev branch) + - The commit SHA + +### Required Variables and Secrets + +The following variables and secrets need to be set in the Gitea repository: + +- `REGISTRY_USERNAME` (variable): Your Docker Hub username +- `REGISTRY_TOKEN` (secret): Your Docker Hub access token + +### Image Naming + +The Docker image will be named using the APP_NAME from the .env file: + +``` +{REGISTRY_USERNAME}/{APP_NAME}:latest +{REGISTRY_USERNAME}/{APP_NAME}:{commit-sha} +``` diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..9d494d3 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,85 @@ +FROM node:20-alpine as builder + +WORKDIR /app + +# Add build arg to bust cache +ARG CACHEBUST=1 + +# Copy package files from frontend directory +COPY frontend/package.json ./ + +# Install dependencies +RUN npm install + +# Copy the frontend application +COPY frontend/ ./ + +# Build the application with cache busting +RUN echo "Building with cache bust: $CACHEBUST" && npm run build + +# Verify build output +RUN ls -la /app/dist || echo "Dist directory not created" +RUN ls -la /app/dist/assets || echo "Assets directory not created" + +# Production stage with FastAPI +FROM python:3.11-alpine as production + +WORKDIR /app + +# Install system dependencies +RUN apk add --no-cache curl + +# Copy Python requirements and install dependencies +COPY backend/requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy built frontend assets from the builder stage +COPY --from=builder /app/dist /app/dist + +# Copy FastAPI application +COPY backend/app /app/app +COPY backend/main.py ./ + +# Expose the port the app runs on +EXPOSE 8080 + +# Add health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8080/api/health || exit 1 + +# Command to run the application +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] + +# Development stage with FastAPI +FROM python:3.11-alpine as development + +WORKDIR /app + +# Install system dependencies +RUN apk add --no-cache curl nodejs npm + +# Copy Python requirements and install dependencies +COPY backend/requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy built frontend assets from the builder stage +COPY --from=builder /app/dist /app/dist + +# Verify the copied frontend build +RUN ls -la /app/dist || echo "Dist directory not created" +RUN ls -la /app/dist/assets || echo "Assets directory not created" + +# Copy FastAPI application +COPY backend/app /app/app +COPY backend/main.py ./ + +# Expose the ports the app runs on (FastAPI and Vite dev server) +EXPOSE 8080 5173 + +# Add health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8080/api/health || exit 1 + +# Command to run the application (FastAPI only in development) +# Frontend will be built and served by FastAPI +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080", "--reload"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..810c3b0 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,80 @@ +# Profile Linker API + +This is a FastAPI backend for the Profile Linker application. It provides API endpoints for managing a list of people's profiles. + +## Project Structure + +The project follows a modular structure: + +``` +backend/ +├── app/ +│ ├── api/ # API endpoints +│ │ ├── endpoints/ # API endpoint modules +│ │ └── api.py # API router +│ ├── core/ # Core application components +│ │ └── config.py # Application settings +│ ├── crud/ # CRUD operations +│ │ └── person.py # Person CRUD operations +│ ├── db/ # Database components +│ │ ├── base.py # Database base +│ │ ├── base_class.py # Base class for database models +│ │ └── session.py # Database session +│ ├── models/ # Database models +│ │ └── person.py # Person model +│ ├── schemas/ # Pydantic schemas +│ │ └── person.py # Person schemas +│ └── main.py # FastAPI application +├── main.py # Entry point +└── requirements.txt # Python dependencies +``` + +## API Endpoints + +The API provides the following endpoints: + +- `GET /api/people`: Get all people +- `POST /api/people`: Create a new person + +## Development + +### Prerequisites + +- Python 3.11+ +- Docker and Docker Compose + +### Running the Application + +#### Using Docker Compose + +1. Build and start the containers: + +```bash +docker-compose up --build +``` + +2. Access the API at http://localhost:8080/api +3. Access the API documentation at http://localhost:8080/docs + +#### Running Locally + +1. Install dependencies: + +```bash +cd backend +pip install -r requirements.txt +``` + +2. Run the application: + +```bash +cd backend +python main.py +``` + +3. Access the API at http://localhost:8080/api +4. Access the API documentation at http://localhost:8080/docs + +## Frontend Integration + +The backend serves the frontend as static assets. The frontend should be built and placed in the `dist` directory. The Docker setup handles this automatically. diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/api.py b/backend/app/api/api.py new file mode 100644 index 0000000..51891ae --- /dev/null +++ b/backend/app/api/api.py @@ -0,0 +1,6 @@ +from fastapi import APIRouter + +from app.api.endpoints import people + +api_router = APIRouter() +api_router.include_router(people.router, prefix="/people", tags=["people"]) diff --git a/backend/app/api/endpoints/__init__.py b/backend/app/api/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/endpoints/people.py b/backend/app/api/endpoints/people.py new file mode 100644 index 0000000..3c8cb32 --- /dev/null +++ b/backend/app/api/endpoints/people.py @@ -0,0 +1,35 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List + +from app.db.session import get_db +from app.schemas.person import Person, PersonCreate +from app.crud import person + +router = APIRouter() + + +@router.get("/", response_model=List[Person]) +def get_all_people(db: Session = Depends(get_db)): + """ + Get all people + """ + return person.get_all(db) + + +@router.post("/", response_model=Person, status_code=201) +def create_person( + *, + db: Session = Depends(get_db), + person_in: PersonCreate +): + """ + Create a new person + """ + try: + return person.create(db=db, obj_in=person_in) + except Exception as e: + raise HTTPException( + status_code=500, + detail="Failed to create the person." + ) diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..a08ec2b --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,25 @@ +from typing import Optional, List +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Simple settings class without using BaseSettings +class Settings: + """ + Application settings + """ + APP_NAME: str = os.getenv("APP_NAME", "ResumeFormatter") + API_V1_STR: str = f"/{APP_NAME}/api" + PROJECT_NAME: str = "Profile Linker API" + + # CORS settings + BACKEND_CORS_ORIGINS: List[str] = ["*"] + + # Database settings - using in-memory database by default + # In a production environment, you would use a real database connection string + DATABASE_URL: Optional[str] = None + + +settings = Settings() diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py new file mode 100644 index 0000000..05a32f3 --- /dev/null +++ b/backend/app/crud/__init__.py @@ -0,0 +1 @@ +from . import person diff --git a/backend/app/crud/person.py b/backend/app/crud/person.py new file mode 100644 index 0000000..37f2b3c --- /dev/null +++ b/backend/app/crud/person.py @@ -0,0 +1,27 @@ +from sqlalchemy.orm import Session +from app.models.person import Person +from app.schemas.person import PersonCreate +import uuid + + +def get_all(db: Session) -> list[Person]: + """ + Get all people from the database + """ + return db.query(Person).all() + + +def create(db: Session, *, obj_in: PersonCreate) -> Person: + """ + Create a new person in the database + """ + db_obj = Person( + id=str(uuid.uuid4()), + firstName=obj_in.firstName, + lastName=obj_in.lastName, + linkedinUrl=obj_in.linkedinUrl + ) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj diff --git a/backend/app/db/base.py b/backend/app/db/base.py new file mode 100644 index 0000000..d661ad8 --- /dev/null +++ b/backend/app/db/base.py @@ -0,0 +1,3 @@ +# Import all the models, so that Base has them before being imported by Alembic +from app.db.base_class import Base # noqa +from app.models.person import Person # noqa diff --git a/backend/app/db/base_class.py b/backend/app/db/base_class.py new file mode 100644 index 0000000..cefea87 --- /dev/null +++ b/backend/app/db/base_class.py @@ -0,0 +1,13 @@ +from typing import Any +from sqlalchemy.ext.declarative import as_declarative, declared_attr + + +@as_declarative() +class Base: + id: Any + __name__: str + + # Generate __tablename__ automatically based on class name + @declared_attr + def __tablename__(cls) -> str: + return cls.__name__.lower() diff --git a/backend/app/db/session.py b/backend/app/db/session.py new file mode 100644 index 0000000..2eac9b5 --- /dev/null +++ b/backend/app/db/session.py @@ -0,0 +1,19 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from app.db.base_class import Base + +# Use a file-based SQLite database instead of in-memory +SQLALCHEMY_DATABASE_URL = "sqlite:///./ResumeFormatter.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Dependency to get DB session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..d728d6f --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,195 @@ +from fastapi import FastAPI, Request +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse +from fastapi.middleware.cors import CORSMiddleware +import os +import pathlib +from dotenv import load_dotenv + +from app.api.api import api_router +from app.core.config import settings +from app.db.session import engine +from app.db.base import Base + +# Load environment variables +load_dotenv() + +# Create tables in the database +Base.metadata.create_all(bind=engine) + +app = FastAPI( + title=settings.PROJECT_NAME, + openapi_url=f"{settings.API_V1_STR}/openapi.json", + docs_url=f"{settings.API_V1_STR}/docs", + redoc_url=f"{settings.API_V1_STR}/redoc" +) + +# Set up CORS +if settings.BACKEND_CORS_ORIGINS: + app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + +# Include API router +app.include_router(api_router, prefix=settings.API_V1_STR) + +# Check if dist directory exists - use absolute path +dist_path = pathlib.Path("/app/dist") +if dist_path.exists(): + # Mount static files from the dist directory if it exists + assets_path = dist_path / "assets" + if assets_path.exists(): + # Mount assets with APP_NAME prefix + app.mount(f"/{settings.APP_NAME}/assets", StaticFiles(directory=str(assets_path)), name="assets") + print(f"Successfully mounted assets from {assets_path} at /{settings.APP_NAME}/assets") +else: + print(f"Warning: Dist directory {dist_path} does not exist") + # Try local development path with frontend folder + local_dist_path = pathlib.Path("frontend/dist") + if local_dist_path.exists(): + local_assets_path = local_dist_path / "assets" + if local_assets_path.exists(): + # Mount assets with APP_NAME prefix + app.mount(f"/{settings.APP_NAME}/assets", StaticFiles(directory=str(local_assets_path)), name="assets") + print(f"Successfully mounted assets from {local_assets_path} at /{settings.APP_NAME}/assets") + + +@app.get("/api/health") +async def health_check(): + return {"status": "ok"} + + +# Redirect root to APP_NAME +@app.get("/") +async def redirect_to_app(): + return RedirectResponse(f"/{settings.APP_NAME}") + + +# Handle index.css request +@app.get("/index.css") +async def serve_css(): + # Try to serve from /app/dist (container path) + css_path = pathlib.Path("/app/dist/index.css") + if css_path.exists(): + return FileResponse(css_path) + + # Try to serve from frontend/dist (local development path) + local_css_path = pathlib.Path("frontend/dist/index.css") + if local_css_path.exists(): + return FileResponse(local_css_path) + + return {"error": "CSS file not found"} + + +# Handle vite.svg request +@app.get("/vite.svg") +async def serve_favicon(): + # Try to serve from /app/dist (container path) + favicon_path = pathlib.Path("/app/dist/vite.svg") + if favicon_path.exists(): + return FileResponse(favicon_path) + + # Try to serve from frontend/dist (local development path) + local_favicon_path = pathlib.Path("frontend/dist/vite.svg") + if local_favicon_path.exists(): + return FileResponse(local_favicon_path) + + return {"error": "Favicon not found"} + + +# Serve index.html for the APP_NAME route +@app.get("/{app_name}") +async def serve_app(app_name: str): + if app_name != settings.APP_NAME: + return {"error": "App not found"} + + # Try to serve from /app/dist (container path) + index_path = pathlib.Path("/app/dist/index.html") + if index_path.exists(): + return FileResponse(index_path) + + # Try to serve from frontend/dist (local development path) + local_index_path = pathlib.Path("frontend/dist/index.html") + if local_index_path.exists(): + return FileResponse(local_index_path) + + # If neither exists, return a simple HTML response + html_content = """ + + + + Profile Linker API + + + +
+

FastAPI Server Running

+
+

The FastAPI server is running correctly, but the frontend build files are not available.

+

To see the frontend, make sure to build it first with npm run build.

+

API endpoints are available at /docs.

+
+
+ + + """ + return HTMLResponse(content=html_content, status_code=200) + + +# Serve index.html for all other routes to support SPA routing +@app.get("/{app_name}/{full_path:path}") +async def serve_spa(app_name: str, full_path: str): + if app_name != settings.APP_NAME: + return {"error": "App not found"} + + # Check if the path is an API route + if full_path.startswith("api/"): + return {"error": "API route not found"} + + # Try to serve from /app/dist (container path) + index_path = pathlib.Path("/app/dist/index.html") + if index_path.exists(): + return FileResponse(index_path) + + # Try to serve from frontend/dist (local development path) + local_index_path = pathlib.Path("frontend/dist/index.html") + if local_index_path.exists(): + return FileResponse(local_index_path) + + # If neither exists, return a simple HTML response + html_content = """ + + + + Profile Linker API + + + +
+

FastAPI Server Running

+
+

The FastAPI server is running correctly, but the frontend build files are not available.

+

To see the frontend, make sure to build it first with npm run build.

+

API endpoints are available at /docs.

+
+
+ + + """ + return HTMLResponse(content=html_content, status_code=200) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models/person.py b/backend/app/models/person.py new file mode 100644 index 0000000..e534036 --- /dev/null +++ b/backend/app/models/person.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, String +from app.db.base_class import Base + + +class Person(Base): + """ + Database model for a person + """ + id = Column(String, primary_key=True, index=True) + firstName = Column(String, nullable=False) + lastName = Column(String, nullable=False) + linkedinUrl = Column(String, nullable=False) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/person.py b/backend/app/schemas/person.py new file mode 100644 index 0000000..34fce30 --- /dev/null +++ b/backend/app/schemas/person.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel, Field, HttpUrl + + +class PersonBase(BaseModel): + """ + Base schema for a person + """ + firstName: str + lastName: str + linkedinUrl: str + + +class PersonCreate(PersonBase): + """ + Schema for creating a new person + """ + pass + + +class PersonInDBBase(PersonBase): + """ + Base schema for a person in the database + """ + id: str + + class Config: + from_attributes = True + + +class Person(PersonInDBBase): + """ + Schema for returning a person + """ + pass diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..940e5ce --- /dev/null +++ b/backend/main.py @@ -0,0 +1,8 @@ +import uvicorn +import os + +if __name__ == "__main__": + # Use environment variables or default values + host = os.getenv("HOST", "0.0.0.0") + port = int(os.getenv("PORT", "8080")) + uvicorn.run("app.main:app", host=host, port=port, reload=True) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..de2fc89 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.110.0 +uvicorn==0.29.0 +python-dotenv==1.0.1 +pydantic==2.6.1 +sqlalchemy==2.0.27 +uuid==1.30 +pydantic-settings==2.1.0 diff --git a/backend/test_api.py b/backend/test_api.py new file mode 100644 index 0000000..7046ec7 --- /dev/null +++ b/backend/test_api.py @@ -0,0 +1,51 @@ +import requests +import json +import time + +# Base URL for the API +BASE_URL = "http://localhost:8080/api" + +def test_get_people(): + """Test the GET /api/people endpoint""" + response = requests.get(f"{BASE_URL}/people") + print("GET /api/people") + print(f"Status Code: {response.status_code}") + print(f"Response: {json.dumps(response.json(), indent=2)}") + print("-" * 50) + return response.json() + +def test_create_person(): + """Test the POST /api/people endpoint""" + new_person = { + "firstName": "Alan", + "lastName": "Turing", + "linkedinUrl": "https://www.linkedin.com/in/alan-turing" + } + response = requests.post(f"{BASE_URL}/people", json=new_person) + print("POST /api/people") + print(f"Status Code: {response.status_code}") + print(f"Response: {json.dumps(response.json(), indent=2)}") + print("-" * 50) + return response.json() + +def run_tests(): + """Run all tests""" + print("Testing API endpoints...") + print("=" * 50) + + # Test GET /api/people + people = test_get_people() + + # Test POST /api/people + new_person = test_create_person() + + # Test GET /api/people again to see the new person + updated_people = test_get_people() + + print("Tests completed!") + +if __name__ == "__main__": + # Wait a bit for the server to start + print("Waiting for server to start...") + time.sleep(2) + run_tests() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..46d5444 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.8' + +services: + ResumeFormatter: + build: + context: . + dockerfile: backend/Dockerfile + target: development + args: + # Add a build arg with current timestamp to force rebuild + CACHEBUST: ${CACHEBUST:-$(date +%s)} + + ports: + - "8080:8080" # FastAPI port + - "5173:5173" # Vite dev server port (if needed) + + volumes: + # Mount backend code for development + - ./backend:/app/backend + # Don't mount frontend - it's built during docker build + + environment: + - NODE_ENV=development + # FastAPI environment variables + - HOST=0.0.0.0 + - PORT=8080 + + restart: unless-stopped diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/App.tsx b/frontend/App.tsx new file mode 100644 index 0000000..40cb5af --- /dev/null +++ b/frontend/App.tsx @@ -0,0 +1,105 @@ + +import React, { useState, useEffect, useCallback } from 'react'; +import type { Person, NewPerson } from './types'; +import { getPeople, addPerson } from './services/apiService'; +import PersonList from './components/PersonList'; +import AddPersonForm from './components/AddPersonForm'; +import Modal from './components/Modal'; +import { PlusIcon, UsersIcon } from './components/Icons'; + +const App: React.FC = () => { + const [people, setPeople] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + const fetchPeople = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + const data = await getPeople(); + setPeople(data); + } catch (err) { + setError('Failed to fetch people. Please try again later.'); + console.error(err); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + fetchPeople(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleAddPerson = async (newPerson: NewPerson) => { + try { + const addedPerson = await addPerson(newPerson); + setPeople((prevPeople) => [...prevPeople, addedPerson]); + setIsModalOpen(false); + } catch (err) { + setError('Failed to add person. Please try again.'); + console.error(err); + } + }; + + const renderContent = () => { + if (isLoading) { + return

Loading profiles...

; + } + if (error) { + return

{error}

; + } + if (people.length === 0) { + return ( +
+ +

No profiles yet

+

Get started by adding a new profile.

+
+ +
+
+ ); + } + return ; + }; + + return ( +
+
+
+

+ Profile Linker +

+ {people.length > 0 && ( + + )} +
+ +
+ {renderContent()} +
+
+ + setIsModalOpen(false)} title="Add New Profile"> + setIsModalOpen(false)} /> + +
+ ); +}; + +export default App; diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..c6b38a7 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1fGMiOc3HONBnOp0qMiclJ_huKwQg8ukW + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/frontend/api_spec.md b/frontend/api_spec.md new file mode 100644 index 0000000..8d89e40 --- /dev/null +++ b/frontend/api_spec.md @@ -0,0 +1,110 @@ + +# Profile Linker API Specification + +This document outlines the API endpoints required for the Profile Linker application. The API is responsible for managing a list of people's profiles. + +## Base URL + +The API endpoints are relative to a base URL, e.g., `https://api.example.com`. + +## Authentication + +All API endpoints could be protected by an authentication mechanism (e.g., OAuth 2.0, API Key in header), which is not detailed in this specification. For this example, we assume endpoints are publicly accessible. + +## Data Models + +### Person Object + +Represents a single person's profile. + +| Field | Type | Description | Example | +|---------------|--------|----------------------------------------|----------------------------------------| +| `id` | string | Unique identifier for the person | `"a1b2c3d4-e5f6-7890-1234-567890abcdef"` | +| `firstName` | string | The person's first name | `"Jane"` | +| `lastName` | string | The person's last name | `"Doe"` | +| `linkedinUrl` | string | Full URL to the person's LinkedIn profile | `"https://www.linkedin.com/in/janedoe"` | + +--- + +## Endpoints + +### 1. Get All People + +Retrieves a list of all people in the database. + +- **URL:** `/api/people` +- **Method:** `GET` +- **Success Response:** + - **Code:** `200 OK` + - **Content:** An array of Person objects. + ```json + [ + { + "id": "1", + "firstName": "Ada", + "lastName": "Lovelace", + "linkedinUrl": "https://www.linkedin.com/in/ada-lovelace" + }, + { + "id": "2", + "firstName": "Grace", + "lastName": "Hopper", + "linkedinUrl": "https://www.linkedin.com/in/grace-hopper" + } + ] + ``` +- **Error Response:** + - **Code:** `500 Internal Server Error` + - **Content:** + ```json + { + "error": "An unexpected error occurred." + } + ``` + +--- + +### 2. Add a New Person + +Creates a new person record in the database. + +- **URL:** `/api/people` +- **Method:** `POST` +- **Request Body:** A JSON object containing the new person's details (excluding the `id`). + ```json + { + "firstName": "Margaret", + "lastName": "Hamilton", + "linkedinUrl": "https://www.linkedin.com/in/margaret-hamilton" + } + ``` +- **Success Response:** + - **Code:** `201 Created` + - **Content:** The newly created Person object, including its server-generated `id`. + ```json + { + "id": "3", + "firstName": "Margaret", + "lastName": "Hamilton", + "linkedinUrl": "https://www.linkedin.com/in/margaret-hamilton" + } + ``` +- **Error Responses:** + - **Code:** `400 Bad Request` (e.g., for missing fields or invalid data) + - **Content:** + ```json + { + "error": "Validation failed.", + "details": { + "firstName": "First name is required.", + "linkedinUrl": "A valid URL is required." + } + } + ``` + - **Code:** `500 Internal Server Error` + - **Content:** + ```json + { + "error": "Failed to create the person." + } + ``` diff --git a/frontend/components/AddPersonForm.tsx b/frontend/components/AddPersonForm.tsx new file mode 100644 index 0000000..bb3aca8 --- /dev/null +++ b/frontend/components/AddPersonForm.tsx @@ -0,0 +1,104 @@ + +import React, { useState } from 'react'; +import type { NewPerson } from '../types'; + +interface AddPersonFormProps { + onSubmit: (person: NewPerson) => void; + onCancel: () => void; +} + +const AddPersonForm: React.FC = ({ onSubmit, onCancel }) => { + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [linkedinUrl, setLinkedinUrl] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!firstName || !lastName || !linkedinUrl) { + setError('All fields are required.'); + return; + } + // Basic URL validation + try { + new URL(linkedinUrl); + } catch (_) { + setError('Please enter a valid LinkedIn URL.'); + return; + } + setError(''); + onSubmit({ firstName, lastName, linkedinUrl }); + }; + + return ( +
+ {error &&
{error}
} +
+ +
+ setFirstName(e.target.value)} + className="appearance-none block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white" + required + /> +
+
+ +
+ +
+ setLastName(e.target.value)} + className="appearance-none block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white" + required + /> +
+
+ +
+ +
+ setLinkedinUrl(e.target.value)} + placeholder="https://www.linkedin.com/in/..." + className="appearance-none block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white" + required + /> +
+
+ +
+ + +
+
+ ); +}; + +export default AddPersonForm; diff --git a/frontend/components/Icons.tsx b/frontend/components/Icons.tsx new file mode 100644 index 0000000..9e6935e --- /dev/null +++ b/frontend/components/Icons.tsx @@ -0,0 +1,27 @@ + +import React from 'react'; + +export const PlusIcon: React.FC> = (props) => ( + + + +); + +export const LinkedInIcon: React.FC> = (props) => ( + + + +); + +export const UsersIcon: React.FC> = (props) => ( + + + +); + +export const CloseIcon: React.FC> = (props) => ( + + + +); + diff --git a/frontend/components/Modal.tsx b/frontend/components/Modal.tsx new file mode 100644 index 0000000..75e9aa6 --- /dev/null +++ b/frontend/components/Modal.tsx @@ -0,0 +1,62 @@ + +import React, { Fragment } from 'react'; +import { CloseIcon } from './Icons'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: React.ReactNode; +} + +const Modal: React.FC = ({ isOpen, onClose, title, children }) => { + if (!isOpen) { + return null; + } + + return ( +
+
+ {/* Background overlay */} + + + {/* This element is to trick the browser into centering the modal contents. */} + + + {/* Modal panel */} +
+
+
+
+
+ + +
+
+ {children} +
+
+
+
+
+
+
+ ); +}; + +export default Modal; diff --git a/frontend/components/PersonList.tsx b/frontend/components/PersonList.tsx new file mode 100644 index 0000000..9d677a8 --- /dev/null +++ b/frontend/components/PersonList.tsx @@ -0,0 +1,57 @@ + +import React from 'react'; +import type { Person } from '../types'; +import { LinkedInIcon } from './Icons'; + +interface PersonListProps { + people: Person[]; +} + +const PersonList: React.FC = ({ people }) => { + return ( +
+
+ + + + + + + + + + {people.map((person) => ( + + + + + + ))} + +
+ First Name + + Last Name + + LinkedIn Profile +
+ {person.firstName} + + {person.lastName} + + + + View Profile + +
+
+
+ ); +}; + +export default PersonList; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..e8488a8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,24 @@ + + + + + + + Profile Linker + + + + + +
+ + + diff --git a/frontend/index.tsx b/frontend/index.tsx new file mode 100644 index 0000000..aaa0c6e --- /dev/null +++ b/frontend/index.tsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); diff --git a/frontend/metadata.json b/frontend/metadata.json new file mode 100644 index 0000000..213c78e --- /dev/null +++ b/frontend/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Profile Linker", + "description": "An application to manage a list of contacts with their first name, last name, and LinkedIn profile URL. It features the ability to add new contacts through a mocked backend service, designed for easy integration with a real API.", + "requestFramePermissions": [] +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1e74f5e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "ResumeFormatter", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react-dom": "^19.2.0", + "react": "^19.2.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts new file mode 100644 index 0000000..fbc284b --- /dev/null +++ b/frontend/services/apiService.ts @@ -0,0 +1,52 @@ +import type { Person, NewPerson } from '../types'; + +// Get APP_NAME from environment or use default +const APP_NAME = import.meta.env.VITE_APP_NAME || 'ResumeFormatter'; +const API_BASE_URL = `/${APP_NAME}/api`; + +// Function to get all people +export const getPeople = async (): Promise => { + console.log('Fetching people from API...'); + try { + const response = await fetch(`${API_BASE_URL}/people/`); + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + const data = await response.json(); + console.log('... Data loaded:', data); + return data; + } catch (error) { + console.error('Error fetching people:', error); + throw error; + } +}; + +// Function to add a new person +export const addPerson = async (newPerson: NewPerson): Promise => { + console.log('Adding person to API:', newPerson); + + if (!newPerson.firstName || !newPerson.lastName || !newPerson.linkedinUrl) { + throw new Error('All fields are required.'); + } + + try { + const response = await fetch(`${API_BASE_URL}/people/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newPerson), + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = await response.json(); + console.log('... Person added:', data); + return data; + } catch (error) { + console.error('Error adding person:', error); + throw error; + } +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} \ No newline at end of file diff --git a/frontend/types.ts b/frontend/types.ts new file mode 100644 index 0000000..6e21bec --- /dev/null +++ b/frontend/types.ts @@ -0,0 +1,9 @@ + +export interface Person { + id: string; + firstName: string; + lastName: string; + linkedinUrl: string; +} + +export type NewPerson = Omit; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..4e5b64f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,31 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + const appName = env.VITE_APP_NAME || 'ResumeFormatter'; + + return { + base: `/${appName}/`, + build: { + assetsDir: 'assets', + outDir: 'dist', + emptyOutDir: true, + }, + server: { + port: 3000, + host: '0.0.0.0', + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +}); diff --git a/new_project.sh b/new_project.sh new file mode 100755 index 0000000..9d26025 --- /dev/null +++ b/new_project.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Check if APP_NAME parameter is provided +if [ -z "$1" ]; then + echo "Error: APP_NAME parameter is required" + echo "Usage: ./new_project.sh " + exit 1 +fi + +# Store the APP_NAME parameter +APP_NAME="$1" +APP_NAME_LOWER=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]') + +echo "Creating new project with name: $APP_NAME" + +# Delete the .git folder +echo "Removing Git history..." +rm -rf .git + +# Replace all references to NEW_APP_NAME with the provided APP_NAME +echo "Replacing NEW_APP_NAME with $APP_NAME in all files..." +find . -type f -not -path "*/\.*" -not -path "./node_modules/*" -not -path "./venv/*" -not -name "new_project.sh" -exec grep -l "NEW_APP_NAME" {} \; | xargs -I{} sed -i '' "s/NEW_APP_NAME/$APP_NAME/g" {} + +# Create .env file with APP_NAME +echo "Creating .env file..." +echo "APP_NAME=$APP_NAME" > .env + +# Initialize a new git repository +echo "Initializing new Git repository..." +git init +git add . +git commit -m "Initial commit for $APP_NAME project" + +# Get the current directory name +CURRENT_DIR=$(basename "$(pwd)") +PARENT_DIR=$(dirname "$(pwd)") + +# Move up one folder and rename the folder to the app name in lowercase +echo "Renaming project folder to $APP_NAME_LOWER..." +cd .. +mv "$CURRENT_DIR" "$APP_NAME_LOWER" + +echo "Project setup complete!" +echo "Your new project is available at: $PARENT_DIR/$APP_NAME_LOWER" +echo "You can now cd into $APP_NAME_LOWER to start working on your project" + +# The script will delete itself when executed from the new directory +# This is handled by the script not including itself in the files to be kept