From 98369ef5314a5a34c208fa94cf1b654e6c53b3a5 Mon Sep 17 00:00:00 2001 From: Giovani Date: Wed, 23 Jul 2025 15:02:32 +0000 Subject: [PATCH] feat: connect backend with the frontend --- .env.example | 8 +++++++- .vscode/extensions.json | 5 +++++ backend/config.py | 7 ++++++- backend/main.py | 9 +++++++++ frontend/.env.example | 4 ++++ frontend/package-lock.json | 1 + frontend/package.json | 1 + frontend/src/api/url/types.ts | 5 +++-- frontend/src/api/url/urlApi.ts | 13 +++++++------ frontend/src/features/url/urlSlice.ts | 7 ++++--- frontend/src/pages/Home.tsx | 14 +++++++------- 11 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 frontend/.env.example diff --git a/.env.example b/.env.example index ac55775..89da06b 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# Backend environment variables # + +# Must be a PostgreSQL db connection string DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase # [dev, stage, prod] @@ -15,4 +18,7 @@ HASHIDS_SALT=default-insecure-salt # random.shuffle(base61) # print("".join(base61)) ### -ENCODER_ALPHABET=CnArvIseYhld2BtZipybguVKaMx4QFkcR71DTLJEP65jUGzqmw9fSoXW83HNO \ No newline at end of file +ENCODER_ALPHABET=CnArvIseYhld2BtZipybguVKaMx4QFkcR71DTLJEP65jUGzqmw9fSoXW83HNO + +### which origins are allowed to make cross origin requests (CORS) +ALLOW_ORIGINS=http://localhost:3000,http://localhost:3030 \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6a67d52..a76bada 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,15 @@ { "recommendations": [ + // Javascript + "dbaeumer.vscode-eslint", + // Python "ms-python.python", "ms-python.debugpy", "ms-python.pylint", + // SQL "mtxr.sqltools", "mtxr.sqltools-driver-pg", + // Misc "humao.rest-client", "genieai.chatgpt-vscode" ], diff --git a/backend/config.py b/backend/config.py index 30b7f61..b0fabe0 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,15 +1,20 @@ from pydantic_settings import BaseSettings -from typing import Literal +from typing import Literal, List class Settings(BaseSettings): database_url: str environment: Literal["dev", "stage", "prod"] hashids_salt: str encoder_alphabet: str + allow_origins: str class Config: env_file = ".env" env_file_encoding = "utf-8" + def get_allow_origins(self) -> List[str]: + return [origin.strip() for origin in self.allow_origins.split(",")] + + # Singleton instance of settings settings = Settings() \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 4d1f2ad..7f0dcd2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from backend.config import settings from backend.db import database, Base, engine @@ -6,6 +7,14 @@ from backend.routes import shortener_router app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=settings.get_allow_origins(), + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + @app.on_event("startup") async def startup(): await database.connect() diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..4fdc244 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,4 @@ +# Frontend environment variables # + +REACT_APP_API_BASE_URL=http://localhost:8000 +REACT_APP_BASE_URL=http://localhost:8000 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index de01e43..9ea24d6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "@types/node": "^16.18.126", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "eslint": "^8.57.1", "react": "^19.1.0", "react-dom": "^19.1.0", "react-redux": "^9.2.0", diff --git a/frontend/package.json b/frontend/package.json index f1efaf8..4194016 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@types/node": "^16.18.126", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "eslint": "^8.57.1", "react": "^19.1.0", "react-dom": "^19.1.0", "react-redux": "^9.2.0", diff --git a/frontend/src/api/url/types.ts b/frontend/src/api/url/types.ts index 8906275..86c7311 100644 --- a/frontend/src/api/url/types.ts +++ b/frontend/src/api/url/types.ts @@ -1,8 +1,9 @@ export interface ShortenUrlRequest { - longUrl: string; + url: string; } export interface ShortenUrlResponse { - shortCode: string; + url: string; + shortcode: string; } \ No newline at end of file diff --git a/frontend/src/api/url/urlApi.ts b/frontend/src/api/url/urlApi.ts index 9ffb16b..1bfce52 100644 --- a/frontend/src/api/url/urlApi.ts +++ b/frontend/src/api/url/urlApi.ts @@ -1,11 +1,11 @@ import { ShortenUrlRequest, ShortenUrlResponse } from "./types"; +const baseUrl = process.env.REACT_APP_API_BASE_URL + export async function shortenUrlApi( payload: ShortenUrlRequest ): Promise { - /* TODO - - const response = await fetch('https://api.minxa.lol/api/v1/url', { + const response = await fetch(`${baseUrl}/`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), @@ -14,10 +14,11 @@ export async function shortenUrlApi( if (!response.ok) { throw new Error('Failed to shorten URL'); } - - */ + + const data = await response.json() return { - shortCode: 'Ux5vy' // Dummy value return + url: data.url, + shortcode: data.shortcode } } \ No newline at end of file diff --git a/frontend/src/features/url/urlSlice.ts b/frontend/src/features/url/urlSlice.ts index 7de2a43..9393851 100644 --- a/frontend/src/features/url/urlSlice.ts +++ b/frontend/src/features/url/urlSlice.ts @@ -5,9 +5,10 @@ import { UrlState } from './types'; export const shortenUrl = createAsyncThunk( 'url/shortenUrl', - async (longUrl: string) => { - const data = await shortenUrlApi({ longUrl }); - return `https://minxa.lol/${data.shortCode}`; + async (url: string) => { + const baseUrl = process.env.REACT_APP_BASE_URL + const data = await shortenUrlApi({ url }); + return `${baseUrl}/${data.shortcode}`; } ); diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 97db98a..c36325c 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -11,7 +11,7 @@ import { const Home: React.FC = () => { /* component level state */ - const [longUrl, setLongUrl] = useState(''); + const [url, setUrl] = useState(''); /* global state */ const dispatch = useAppDispatch(); @@ -22,8 +22,8 @@ const Home: React.FC = () => { /* methods */ const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && longUrl.trim() !== '') { - dispatch(shortenUrl(longUrl)); + if (e.key === 'Enter' && url.trim() !== '') { + dispatch(shortenUrl(url)); } }; @@ -42,9 +42,9 @@ const Home: React.FC = () => { setLongUrl(e.target.value)} + placeholder="https://example.com/my-long-url" + value={url} + onChange={(e) => setUrl(e.target.value)} onKeyDown={handleKeyDown} className="w-80 p-3 rounded-md text-lg border border-gray-300 shadow-sm focus:outline-none focus:ring-2 focus:ring-orange-400" /> @@ -77,7 +77,7 @@ const Home: React.FC = () => { onClick={handleCopy} className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600 transition"> Copy to Clipboard - + )}