diff --git a/GitReadMe.md b/GitReadMe.md new file mode 100644 index 0000000..9caf620 --- /dev/null +++ b/GitReadMe.md @@ -0,0 +1,15 @@ +1. initial commit with its feature name + +2. git reset --soft #hash: undoing a commit (like to see changes that youve made) + +3. git commit -m "message" to add the commit (to add this new change) + +4. git commit --amend --no-edit (changes the last commit) + +"Feature homepage" +initial commit - "feature homepage" +commit your new changes "feature homepage" + +git log +git status +git push --force (anytime you have different commits between local and remote) diff --git a/ReadMe.md b/ReadMe.md index 6f4b599..5892c18 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,15 +1,52 @@ -1. initial commit with its feature name +# Blog App – Full Stack Project -2. git reset --soft: undoing a commit (like to see changes that youve made) +## Overview -3. git commit -m "message" to add the commit (to add this new change) +This is a full stack blog application where users can sign up, log in, create posts (with optional images), comment on posts, and like/unlike posts. The project demonstrates a modern web application architecture using a **React** frontend and a **FastAPI** backend, with a **PostgreSQL** database for persistent storage. -4. git commit --amend (changes the last commit) +--- -"Feature homepage" -initial commit - "feature homepage" -commit your new changes "feature homepage" +## Features -git log -git status -git push --force (anytime you have different commits between local and remote) +- **User Authentication:** Sign up and log in with JWT-based authentication. +- **Create Posts:** Users can create text posts and optionally upload images. +- **View Posts:** All posts are displayed in reverse chronological order. +- **Commenting System:** Users can comment on any post. +- **Like System:** Users can like or unlike posts. +- **Responsive UI:** Built with React and Tailwind CSS for a modern look. +- **Image Uploads:** Uploaded images are stored and served from the backend. +- **Admin/Feature Roadmap:** (Planned) Admin dashboard, notifications, user profiles, and post categorization. + +--- + +## Technologies Used + +### **Frontend** + +- **React** (with Vite for fast development) +- **React Router** (for navigation) +- **Axios** (for HTTP requests) +- **Tailwind CSS** (for styling) +- **React Icons** (for UI icons) + +### **Backend** + +- **FastAPI** (Python web framework) +- **SQLAlchemy** (ORM for database models) +- **Pydantic** (for data validation) +- **bcrypt** (for password hashing) +- **python-jose** (for JWT authentication) +- **python-dotenv** (for environment variable management) +- **Uvicorn** (ASGI server for FastAPI) + +### **Database** + +- **PostgreSQL** (relational database) + +### **DevOps/Deployment** + +- **Docker & Docker Compose** (for containerization and easy local development) +- **Nginx** (serving the frontend in production) +- **Render** (for cloud deployment, if used) + +--- diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..7aa2e34 --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,30 @@ +import os +# Load environment variables from .env file +from dotenv import load_dotenv +load_dotenv() + +from fastapi import HTTPException, status +from datetime import datetime, timezone, timedelta +from jose import jwt, JWTError + + +def create_access_token(data: dict, expires_delta: int = 3600): + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + timedelta(seconds=expires_delta) + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, os.environ.get("JWT_SECRET_KEY") , algorithm=os.environ.get("JWT_ALGORITHM")) # Use a secure secret key + return encoded_jwt + +def decode_access_token(token: str): + try: + payload = jwt.decode(token, os.environ.get("JWT_SECRET_KEY"), algorithms=[os.environ.get("JWT_ALGORITHM")]) + return payload + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token", + headers={"WWW-Authenticate": "Bearer"}, + ) diff --git a/backend/database-schema.sql b/backend/database-schema.sql index 36589a4..34323ce 100644 --- a/backend/database-schema.sql +++ b/backend/database-schema.sql @@ -8,4 +8,31 @@ CREATE TABLE users ( hashed_password TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE posts ( + post_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(user_id) ON DELETE CASCADE, + username TEXT NOT NULL, + text TEXT NOT NULL, + image_filename TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE comments ( + comment_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + post_id UUID REFERENCES posts(post_id) ON DELETE CASCADE, + user_id UUID REFERENCES users(user_id) ON DELETE CASCADE, + username TEXT NOT NULL, + text TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE likes ( + like_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + post_id UUID REFERENCES posts(post_id) ON DELETE CASCADE, + user_id UUID REFERENCES users(user_id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 0745d11..29ac2f6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,8 +1,13 @@ -from fastapi import FastAPI, HTTPException, status, Depends +import uuid +from fastapi import FastAPI, HTTPException, status, Depends, APIRouter, Form, UploadFile, File from fastapi.middleware.cors import CORSMiddleware +from fastapi.security import OAuth2PasswordBearer +from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session from database import SessionLocal -from model import UserModel, SignUpRequest, LoginRequest +from auth import decode_access_token, create_access_token +from model import UserModel, SignUpRequest, LoginRequest, PostModel, CommentModel, LikeModel +import os import bcrypt @@ -14,15 +19,20 @@ app = FastAPI() +# FASTAPI security for OAuth2 +oauth2_scheme = OAuth2PasswordBearer(tokenUrl = "/login") # Placeholder for OAuth2 scheme if needed + + + origins = [ - "https://blog-app-fe-be.onrender.com", #render production + "https://blog-app-fe-be.onrender.com", #frontend production server + "https://blog-app-y08k.onrender.com", #backend production server "http://localhost:5173", #local development "http://localhost" #docker compose - ] app.add_middleware( CORSMiddleware, - allow_origins=origins, + allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -35,10 +45,12 @@ def get_db(): finally: db.close() + @app.get("/") -def read_root(): - return {"message": "Hello shmoe mama 123"} +def root(): + return {"message": "Backend is running"} +#public endpoints for signup and login @app.post("/signup") def signup(request: SignUpRequest, db: Session = Depends(get_db)): existing_user = db.query(UserModel).filter( @@ -84,11 +96,170 @@ def login(request: LoginRequest, db: Session = Depends(get_db)): detail="Incorrect password" ) + #creating access token jwt + access_token = create_access_token(data={"sub": str(user.user_id), "username": user.username}) return { + "access_token": access_token, + "token_type": "bearer", "message": "Login successful", "user": { "user_id": str(user.user_id), "username": user.username, "email": user.email } - } \ No newline at end of file + } + +def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): + payload = decode_access_token(token) + if not payload: + raise HTTPException(status_code=401, detail="Invalid token") + user_id = payload.get("sub") + user = db.query(UserModel).filter(UserModel.user_id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + + +# Protected endpoints that require authentication +auth_router = APIRouter(prefix="/auth") # allows for routing of auth related endpoints +@auth_router.get("/home") +def read_users_me(current_user: UserModel = Depends(get_current_user)): + return { + "message": f"Welcome {current_user.username}!", + "user_id": str(current_user.user_id), + "username": current_user.username, + "email": current_user.email + } + +UPLOAD_DIR = "static/images" +os.makedirs(UPLOAD_DIR, exist_ok=True) +@auth_router.post("/posts") +def create_post( + text:str = Form(...), + image: UploadFile = File(None), + db: Session = Depends(get_db), + current_user: UserModel = Depends(get_current_user) +): + image_filename = None + if image: + image_filename = f"{uuid.uuid4()}_{image.filename}" + with open(os.path.join(UPLOAD_DIR, image_filename), "wb") as buffer: + buffer.write(image.file.read()) + post = PostModel( + user_id=current_user.user_id, + username=current_user.username, + text=text, + image_filename=image_filename, + ) + db.add(post) + db.commit() + db.refresh(post) + return { + "message": "Post created successfully", + "post": { + "post_id": str(post.post_id), + "username": post.username, + "text": post.text, + "image_filename": post.image_filename, + "created_at": post.created_at.isoformat() if post.created_at else None, + } + } + + +@auth_router.get("/posts") +def get_posts( + db: Session = Depends(get_db), + current_user: UserModel = Depends(get_current_user) +): + posts = db.query(PostModel).order_by(PostModel.created_at.desc()).all() + result = [] + for post in posts: + likes_count = db.query(LikeModel).filter(LikeModel.post_id == post.post_id).count() + comments_count = db.query(CommentModel).filter(CommentModel.post_id == post.post_id).count() + result.append({ + "post_id": str(post.post_id), + "user_id": str(post.user_id), + "username": post.username, + "text": post.text, + "image_filename": post.image_filename, + "created_at": post.created_at.isoformat() if post.created_at else None, + "updated_at": post.updated_at.isoformat() if post.updated_at else None, + "likes_count": likes_count, + "comments_count": comments_count + }) + return result +app.mount("/images", StaticFiles(directory=UPLOAD_DIR), name="images") + +# Creating and Getting Comments +@auth_router.post("/posts/{post_id}/comments") +def create_comment( + post_id: str, + text: str = Form(...), + db: Session = Depends(get_db), + current_user: UserModel = Depends(get_current_user) +): + post = db.query(PostModel).filter(PostModel.post_id == post_id).first() + if not post: + raise HTTPException(status_code = 404, detail = "Post not found") + comment = CommentModel( + post_id=post.post_id, + user_id=current_user.user_id, + username=current_user.username, + text=text + ) + db.add(comment) + db.commit() + db.refresh(comment) + return { + "message": "Comment created successfully", + "comment": { + "comment_id": str(comment.comment_id), + "post_id": str(comment.post_id), + "user_id": str(comment.user_id), + "username": comment.username, + "text": comment.text, + "created_at": comment.created_at.isoformat() if comment.created_at else None, + } + } +@auth_router.get("/posts/{post_id}/comments") +def get_comments( + post_id: str, + db: Session = Depends(get_db), + current_user: UserModel = Depends(get_current_user) +): + post = db.query(PostModel).filter(PostModel.post_id == post_id).first() + if not post: + raise HTTPException(status_code=404, detail="Post not found") + + comments = db.query(CommentModel).filter(CommentModel.post_id == post_id).order_by(CommentModel.created_at.desc()).all() + return [ + { + "comment_id": str(comment.comment_id), + "post_id": str(comment.post_id), + "user_id": str(comment.user_id), + "username": comment.username, + "text": comment.text, + "created_at": comment.created_at.isoformat() if comment.created_at else None, + } + for comment in comments + ] + +# Creating and Getting Likes +@auth_router.post("/posts/{post_id}/like") +def like_post(post_id: str, db: Session = Depends(get_db), current_user: UserModel = Depends(get_current_user)): + existing_like = db.query(LikeModel).filter_by(post_id=post_id, user_id=current_user.user_id).first() + if existing_like: + db.delete(existing_like) + db.commit() + return {"message": "Post unliked"} + like = LikeModel(post_id=post_id, user_id=current_user.user_id) + db.add(like) + db.commit() + return {"message": "Post liked"} + +@auth_router.get("/posts/{post_id}/likes") +def get_likes(post_id: str, db: Session = Depends(get_db), current_user: UserModel = Depends(get_current_user)): + count = db.query(LikeModel).filter_by(post_id=post_id).count() + return {"likes_count": count} + +app.include_router(auth_router) diff --git a/backend/model.py b/backend/model.py index 3fe3b9c..b33def7 100644 --- a/backend/model.py +++ b/backend/model.py @@ -1,5 +1,5 @@ # database models -from sqlalchemy import Column, String, DateTime +from sqlalchemy import Column, String, DateTime, ForeignKey, Text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.sql import func from database import Base @@ -16,6 +16,33 @@ class UserModel(Base): updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now()) +class PostModel(Base): + __tablename__ = "posts" + post_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True, nullable=False) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False) + username = Column(String, nullable=False) + text = Column(Text, nullable=False) + image_filename = Column(String, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now()) + +class CommentModel(Base): + __tablename__ = "comments" + comment_id = Column(UUID(as_uuid=True), primary_key = True, default = uuid.uuid4, unique=True, nullable=False) + post_id = Column(UUID(as_uuid=True), ForeignKey("posts.post_id"), nullable=False) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False) + username = Column(String, nullable=False) + text = Column(Text, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now()) + +class LikeModel(Base): + __tablename__ = "likes" + like_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True, nullable=False) + post_id = Column(UUID(as_uuid=True), ForeignKey("posts.post_id"), nullable=False) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable = False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + #difference between BaseModel and Base #Base = sql specific Model #BaseModel = pydantic specific Model diff --git a/backend/pyproject.toml b/backend/pyproject.toml index bd59caf..94911bb 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -7,7 +7,11 @@ requires-python = ">=3.13" dependencies = [ "bcrypt>=4.3.0", "fastapi>=0.115.12", + "passlib[bcrypt]>=1.7.4", "psycopg2-binary>=2.9.10", "pydantic>=2.11.5", + "python-dotenv>=1.1.0", + "python-jose[cryptography]>=3.5.0", + "python-multipart>=0.0.20", "sqlalchemy>=2.0.41", ] diff --git a/backend/uv.lock b/backend/uv.lock index 6610700..2bb5b29 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -31,8 +31,12 @@ source = { virtual = "." } dependencies = [ { name = "bcrypt" }, { name = "fastapi" }, + { name = "passlib", extra = ["bcrypt"] }, { name = "psycopg2-binary" }, { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "python-jose", extra = ["cryptography"] }, + { name = "python-multipart" }, { name = "sqlalchemy" }, ] @@ -40,8 +44,12 @@ dependencies = [ requires-dist = [ { name = "bcrypt", specifier = ">=4.3.0" }, { name = "fastapi", specifier = ">=0.115.12" }, + { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "pydantic", specifier = ">=2.11.5" }, + { name = "python-dotenv", specifier = ">=1.1.0" }, + { name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" }, + { name = "python-multipart", specifier = ">=0.0.20" }, { name = "sqlalchemy", specifier = ">=2.0.41" }, ] @@ -95,6 +103,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, ] +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "cryptography" +version = "45.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712, upload-time = "2025-06-10T00:02:38.826Z" }, + { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" }, + { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" }, + { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769, upload-time = "2025-06-10T00:02:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441, upload-time = "2025-06-10T00:03:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836, upload-time = "2025-06-10T00:03:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" }, + { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" }, + { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557, upload-time = "2025-06-10T00:03:22.563Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508, upload-time = "2025-06-10T00:03:24.586Z" }, +] + +[[package]] +name = "ecdsa" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" }, +] + [[package]] name = "fastapi" version = "0.115.12" @@ -142,6 +219,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, +] + +[package.optional-dependencies] +bcrypt = [ + { name = "bcrypt" }, +] + [[package]] name = "psycopg2-binary" version = "2.9.10" @@ -161,6 +252,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + [[package]] name = "pydantic" version = "2.11.5" @@ -204,6 +313,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, +] + +[[package]] +name = "python-jose" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ecdsa" }, + { name = "pyasn1" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" }, +] + +[package.optional-dependencies] +cryptography = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" diff --git a/docker-compose.yml b/docker-compose.yml index f07aa5b..90421e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: build: context: ./frontend args: - VITE_API_URL: http://localhost:8000 + VITE_API_URL: http:s//localhost:8000 ports: - "80:80" database: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c2f2a67..8a5766c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,7 @@ import LoginPage from "./pages/loginPage"; import SignUpPage from "./pages/signUpPage"; import HomePage from "./pages/homePage"; import PostsPage from "./pages/postsPage"; +import FeaturesPage from "./pages/featuresPage"; import { Route, createBrowserRouter, @@ -26,8 +27,9 @@ const router = createBrowserRouter( } /> } /> }> - } /> - } /> + } /> + } /> + } /> ) diff --git a/frontend/src/components/CommentForm.jsx b/frontend/src/components/CommentForm.jsx new file mode 100644 index 0000000..4b0c301 --- /dev/null +++ b/frontend/src/components/CommentForm.jsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { createComment } from "../services/api"; + +const CommentForm = ({ postId, onCommentAdded }) => { + const [text, setText] = useState(""); + const [error, setError] = useState(null); + + const handleSubmit = async (e) => { + e.preventDefault(); + const token = localStorage.getItem("access_token"); + + try { + await createComment(token, postId, text); + setText(""); + setError(null); + onCommentAdded(); + } catch (err) { + setError("Failed to create comment."); + console.error(err); + } + }; + return ( + <> +
+ { + setText(e.target.value); + setError(null); + }} + placeholder="Add a comment..." + class="flex-grow bg-gray-200 rounded-full border-gray-500 border-2 p-2 " + required + /> + + {error &&

{error}

} +
+ + ); +}; + +export default CommentForm; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 3160df3..237a960 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -4,12 +4,15 @@ export default function Header() { return (
{/* Critical: This renders the child routes */} diff --git a/frontend/src/components/NewPostForm.jsx b/frontend/src/components/NewPostForm.jsx new file mode 100644 index 0000000..fa9a02b --- /dev/null +++ b/frontend/src/components/NewPostForm.jsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import { createPost } from "../services/api"; +import { FcStackOfPhotos } from "react-icons/fc"; + +const NewPostForm = ({ onPostCreated }) => { + const [text, setText] = useState(""); + const [image, setImage] = useState(null); + const [error, setError] = useState(null); + + const handleSubmit = async (e) => { + e.preventDefault(); + const token = localStorage.getItem("access_token"); + const formData = new FormData(); + formData.append("text", text); + if (image) formData.append("image", image); + + try { + await createPost(token, formData); + setText(""); + setImage(null); + setError(null); + onPostCreated(); // Refresh posts + } catch (err) { + setError("Failed to create post.", err); + } + }; + + return ( +
+