-
+
-
+ Get started by editing{" "}
+
+ src/app/page.tsx ++ . +
+ - Save and see your changes instantly. +
diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..91bc5d2 --- /dev/null +++ b/.env.template @@ -0,0 +1,25 @@ +# Common settings +PROJECT_NAME=EventSpace +VERSION=1.0.0 + +# Database settings +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=eventspace +POSTGRES_HOST=db +POSTGRES_PORT=5432 +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + +# Backend settings +BACKEND_PORT=8000 +SECRET_KEY=your_secret_key_here +ENVIRONMENT=development +DEBUG=true +BACKEND_CORS_ORIGINS=["http://localhost:3000"] +FIRST_SUPERUSER_EMAIL=admin@example.com +FIRST_SUPERUSER_PASSWORD=admin123 + +# Frontend settings +FRONTEND_PORT=3000 +NEXT_PUBLIC_API_URL=http://localhost:8000 +NODE_ENV=development \ No newline at end of file diff --git a/.gitignore b/.gitignore index cf8c060..64381b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,173 +1,3 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - # ---> Node # Logs logs @@ -300,3 +130,178 @@ dist .yarn/install-state.gz .pnp.* +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.env.* +!.env.template +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# IntelliJ IDEA +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ +*.iml + +# Docker volumes +postgres_data*/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2c83dbd --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 cardosofelipe + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bd3bc82 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: dev prod down clean + +VERSION ?= latest +REGISTRY := gitea.pragmazest.com/cardosofelipe/eventspace + + +dev: + docker compose -f docker-compose.dev.yml up --build -d + +prod: + docker compose up --build -d + +down: + docker compose down + +deploy: + docker compose -f docker-compose.deploy.yml pull + docker compose -f docker-compose.deploy.yml up -d + +clean: + docker compose down - + +push-images: + docker build -t $(REGISTRY)/backend:$(VERSION) ./backend + docker build -t $(REGISTRY)/frontend:$(VERSION) ./frontend + docker push $(REGISTRY)/backend:$(VERSION) + docker push $(REGISTRY)/frontend:$(VERSION) \ No newline at end of file diff --git a/README.md b/README.md index 20b256e..33e6bdc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,59 @@ -# fast-next-template +# FastNext Stack -FastNext Stack is a modern full-stack template combining FastAPI, Next.js, and PostgreSQL in a Docker-ready environment. It provides a production-grade foundation for building scalable web applications with TypeScript frontend and Python backend. \ No newline at end of file +A modern, Docker-ready full-stack template combining FastAPI, Next.js, and PostgreSQL. Built for developers who need a robust starting point for web applications with TypeScript frontend and Python backend. + +## Features + +- 🐍 **FastAPI Backend** + - Python 3.12 with modern async support + - SQLAlchemy ORM with async capabilities + - Alembic migrations + - JWT authentication ready + - Pydantic data validation + - Comprehensive testing setup + +- ⚛️ **Next.js Frontend** + - React 19 with TypeScript + - Tailwind CSS for styling + - Modern app router architecture + - Built-in API route support + - SEO-friendly by default + +- 🛠️ **Development Experience** + - Docker-based development environment + - Hot-reloading for both frontend and backend + - Unified development workflow + - Comprehensive testing setup + - Type safety across the stack + +- 🚀 **Production Ready** + - Multi-stage Docker builds + - Production-optimized configurations + - Environment-based settings + - Health checks and container orchestration + - CORS security configured + +## Quick Start + +1. Clone the template: +```bash +git clone https://github.com/yourusername/fastnext-stack myproject +cd myproject +``` + +2. Create environment files: +```bash +cp .env.template .env +``` + +3. Start development environment: +```bash +make dev +``` + +4. Access the applications: +- Frontend: http://localhost:3000 +- Backend: http://localhost:8000 +- API Docs: http://localhost:8000/docs + +## Project Structure diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..4b178df --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,2 @@ +.venv +*.iml \ No newline at end of file diff --git a/backend/=1.14.1 b/backend/=1.14.1 new file mode 100644 index 0000000..271ff8a --- /dev/null +++ b/backend/=1.14.1 @@ -0,0 +1,6 @@ +Requirement already satisfied: alembic in ./.venv/lib/python3.12/site-packages (1.14.1) +Requirement already satisfied: SQLAlchemy>=1.3.0 in ./.venv/lib/python3.12/site-packages (from alembic) (2.0.38) +Requirement already satisfied: Mako in ./.venv/lib/python3.12/site-packages (from alembic) (1.3.9) +Requirement already satisfied: typing-extensions>=4 in ./.venv/lib/python3.12/site-packages (from alembic) (4.12.2) +Requirement already satisfied: greenlet!=0.4.17 in ./.venv/lib/python3.12/site-packages (from SQLAlchemy>=1.3.0->alembic) (3.1.1) +Requirement already satisfied: MarkupSafe>=0.9.2 in ./.venv/lib/python3.12/site-packages (from Mako->alembic) (3.0.2) diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..625a92b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,34 @@ +# Development stage +FROM python:3.12-slim AS development +WORKDIR /app +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app +RUN apt-get update && \ + apt-get install -y --no-install-recommends gcc postgresql-client && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +COPY entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint.sh +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] + +# Production stage +FROM python:3.12-slim AS production +WORKDIR /app +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app +RUN apt-get update && \ + apt-get install -y --no-install-recommends postgresql-client && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +COPY entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint.sh +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..8794f31 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,37 @@ +[alembic] +script_location = alembic +sqlalchemy.url = postgresql://postgres:postgres@db:5432/eventspace + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S \ No newline at end of file diff --git a/backend/alembic/README b/backend/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..36112a3 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,78 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/7396957cbe80_initial_empty_migration.py b/backend/alembic/versions/7396957cbe80_initial_empty_migration.py new file mode 100644 index 0000000..bd34663 --- /dev/null +++ b/backend/alembic/versions/7396957cbe80_initial_empty_migration.py @@ -0,0 +1,26 @@ +"""Initial empty migration + +Revision ID: 7396957cbe80 +Revises: +Create Date: 2025-02-27 12:47:46.445313 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7396957cbe80' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..36ceecd --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,29 @@ +from pydantic_settings import BaseSettings +from typing import Optional, List + + +class Settings(BaseSettings): + PROJECT_NAME: Optional[str] = "EventSpace" + VERSION: Optional[str] = "1.0.0" + API_V1_STR: Optional[str] = "/api/v1" + + # Database configuration + DATABASE_URL: Optional[str] = None + + # JWT configuration + SECRET_KEY: Optional[str] = None + ALGORITHM: Optional[str] = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: Optional[int] = 30 + + # CORS configuration + BACKEND_CORS_ORIGINS: Optional[List[str]] = ["http://localhost:3000"] # Frontend URL + + # Admin user + FIRST_SUPERUSER_EMAIL: Optional[str] = None + FIRST_SUPERUSER_PASSWORD: Optional[str] = None + + class Config: + env_file = ".env" + + +settings = Settings() \ No newline at end of file diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..c557614 --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,20 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os + +SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@db:5432/eventspace") + +engine = create_engine(SQLALCHEMY_DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +# Dependency to get DB session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..044c3aa --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,36 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse + +from app.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + openapi_url=f"{settings.API_V1_STR}/openapi.json" +) + +# Set up CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=settings.BACKEND_CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/", response_class=HTMLResponse) +async def root(): + return """ + +
+Explore the available endpoints and documentation:
+ OpenAPI Documentation + + + """ diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100644 index 0000000..fbf5b1d --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +echo "Starting Backend" + +# Apply database migrations +alembic upgrade head +# Execute the command passed to docker run +exec "$@" \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..94bd973 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,44 @@ +# Core FastAPI framework and dependencies +fastapi>=0.115.8 +uvicorn>=0.34.0 +pydantic>=2.10.6 +pydantic-settings>=2.2.1 +python-multipart>=0.0.19 + +# Database +sqlalchemy>=2.0.29 +alembic>=1.14.1 +psycopg2-binary>=2.9.9 +asyncpg>=0.29.0 + +# Security and authentication +python-jose>=3.4.0 +passlib>=1.7.4 +bcrypt>=4.1.2 +python-dotenv>=1.0.1 + +# API documentation +email-validator>=2.1.0.post1 +ujson>=5.9.0 + +# CORS support +starlette>=0.40.0 +starlette-csrf>=1.4.5 + +# Utilities +httpx>=0.27.0 +tenacity>=8.2.3 +pytz>=2024.1 +pillow>=10.3.0 + +# Testing +pytest>=8.0.0 +pytest-asyncio>=0.23.5 +pytest-cov>=4.1.0 +requests>=2.32.0 + +# Development tools +black>=24.3.0 +isort>=5.13.2 +flake8>=7.0.0 +mypy>=1.8.0 \ No newline at end of file diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..5c848ba --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,74 @@ +services: + db: + image: postgres:17-alpine + volumes: + - postgres_data_dev:/var/lib/postgresql/data/ + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - app-network + + backend: + build: + context: ./backend + dockerfile: Dockerfile + target: development + volumes: + - ./backend:/app + - ./uploads:/app/uploads + - backend_dev_modules:/app/.venv + ports: + - "8000:8000" + environment: + - DATABASE_URL=${DATABASE_URL} + - SECRET_KEY=${SECRET_KEY} + - ENVIRONMENT=development + - DEBUG=true + - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} + depends_on: + db: + condition: service_healthy + networks: + - app-network + command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + target: deps + args: + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + volumes: + - ./frontend:/app + - frontend_dev_modules:/app/node_modules + - frontend_dev_next:/app/.next + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + depends_on: + - backend + command: npm run dev + networks: + - app-network + +volumes: + postgres_data_dev: + backend_dev_modules: + frontend_dev_modules: + frontend_dev_next: + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8bad19 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +services: + db: + image: postgres:17-alpine + volumes: + - postgres_data:/var/lib/postgresql/data/ + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - app-network + restart: unless-stopped + + backend: + build: + context: ./backend + dockerfile: Dockerfile + target: production + environment: + - DATABASE_URL=${DATABASE_URL} + - SECRET_KEY=${SECRET_KEY} + - ENVIRONMENT=production + - DEBUG=false + - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} + depends_on: + db: + condition: service_healthy + networks: + - app-network + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + target: runner + args: + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + depends_on: + - backend + networks: + - app-network + restart: unless-stopped + +volumes: + postgres_data: + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..f1c551f --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.next +*.log +.git +.env* +.dockerignore +Dockerfile \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..65e4b19 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,36 @@ +# Stage 1: Dependencies +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +# Stage 2: Builder +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED 1 +RUN npm run build + +# Stage 3: Runner +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/entrypoint.sh b/frontend/entrypoint.sh new file mode 100644 index 0000000..d36e3f4 --- /dev/null +++ b/frontend/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Wait for backend to be ready +echo "Waiting for backend..." +until nc -z backend 8000; do + sleep 1 +done +echo "Backend is up!" + +# Start the Next.js application +exec "$@" \ No newline at end of file diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..c85fb67 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..fb75eab --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,16 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: 'standalone', + // Ensure we can connect to the backend in Docker + async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'http://backend:8000/:path*', + }, + ]; + }, +}; + +export default nextConfig; \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..91e81f8 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "docker:build": "docker build -t eventspace-frontend .", + "docker:run": "docker run -p 3000:3000 eventspace-frontend" + + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "next": "15.2.0" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@tailwindcss/postcss": "^4", + "tailwindcss": "^4", + "eslint": "^9", + "eslint-config-next": "15.2.0", + "@eslint/eslintrc": "^3" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/frontend/public/file.svg b/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/frontend/src/app/favicon.ico differ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..947a048 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,24 @@ +@import "tailwindcss"; + +@theme { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..3eee014 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,101 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+ src/app/page.tsx
+
+ .
+