feat: connect backend with the frontend

This commit is contained in:
2025-07-23 15:02:32 +00:00
parent 7abed7e634
commit 98369ef531
11 changed files with 54 additions and 20 deletions

View File

@@ -1,3 +1,6 @@
# Backend environment variables #
# Must be a PostgreSQL db connection string
DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase
# [dev, stage, prod] # [dev, stage, prod]
@@ -15,4 +18,7 @@ HASHIDS_SALT=default-insecure-salt
# random.shuffle(base61) # random.shuffle(base61)
# print("".join(base61)) # print("".join(base61))
### ###
ENCODER_ALPHABET=CnArvIseYhld2BtZipybguVKaMx4QFkcR71DTLJEP65jUGzqmw9fSoXW83HNO ENCODER_ALPHABET=CnArvIseYhld2BtZipybguVKaMx4QFkcR71DTLJEP65jUGzqmw9fSoXW83HNO
### which origins are allowed to make cross origin requests (CORS)
ALLOW_ORIGINS=http://localhost:3000,http://localhost:3030

View File

@@ -1,10 +1,15 @@
{ {
"recommendations": [ "recommendations": [
// Javascript
"dbaeumer.vscode-eslint",
// Python
"ms-python.python", "ms-python.python",
"ms-python.debugpy", "ms-python.debugpy",
"ms-python.pylint", "ms-python.pylint",
// SQL
"mtxr.sqltools", "mtxr.sqltools",
"mtxr.sqltools-driver-pg", "mtxr.sqltools-driver-pg",
// Misc
"humao.rest-client", "humao.rest-client",
"genieai.chatgpt-vscode" "genieai.chatgpt-vscode"
], ],

View File

@@ -1,15 +1,20 @@
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from typing import Literal from typing import Literal, List
class Settings(BaseSettings): class Settings(BaseSettings):
database_url: str database_url: str
environment: Literal["dev", "stage", "prod"] environment: Literal["dev", "stage", "prod"]
hashids_salt: str hashids_salt: str
encoder_alphabet: str encoder_alphabet: str
allow_origins: str
class Config: class Config:
env_file = ".env" env_file = ".env"
env_file_encoding = "utf-8" 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 # Singleton instance of settings
settings = Settings() settings = Settings()

View File

@@ -1,4 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from backend.config import settings from backend.config import settings
from backend.db import database, Base, engine from backend.db import database, Base, engine
@@ -6,6 +7,14 @@ from backend.routes import shortener_router
app = FastAPI() app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=settings.get_allow_origins(),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.on_event("startup") @app.on_event("startup")
async def startup(): async def startup():
await database.connect() await database.connect()

4
frontend/.env.example Normal file
View File

@@ -0,0 +1,4 @@
# Frontend environment variables #
REACT_APP_API_BASE_URL=http://localhost:8000
REACT_APP_BASE_URL=http://localhost:8000

View File

@@ -17,6 +17,7 @@
"@types/node": "^16.18.126", "@types/node": "^16.18.126",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"eslint": "^8.57.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",

View File

@@ -12,6 +12,7 @@
"@types/node": "^16.18.126", "@types/node": "^16.18.126",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"eslint": "^8.57.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",

View File

@@ -1,8 +1,9 @@
export interface ShortenUrlRequest { export interface ShortenUrlRequest {
longUrl: string; url: string;
} }
export interface ShortenUrlResponse { export interface ShortenUrlResponse {
shortCode: string; url: string;
shortcode: string;
} }

View File

@@ -1,11 +1,11 @@
import { ShortenUrlRequest, ShortenUrlResponse } from "./types"; import { ShortenUrlRequest, ShortenUrlResponse } from "./types";
const baseUrl = process.env.REACT_APP_API_BASE_URL
export async function shortenUrlApi( export async function shortenUrlApi(
payload: ShortenUrlRequest payload: ShortenUrlRequest
): Promise<ShortenUrlResponse> { ): Promise<ShortenUrlResponse> {
/* TODO const response = await fetch(`${baseUrl}/`, {
const response = await fetch('https://api.minxa.lol/api/v1/url', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload), body: JSON.stringify(payload),
@@ -14,10 +14,11 @@ export async function shortenUrlApi(
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to shorten URL'); throw new Error('Failed to shorten URL');
} }
*/ const data = await response.json()
return { return {
shortCode: 'Ux5vy' // Dummy value return url: data.url,
shortcode: data.shortcode
} }
} }

View File

@@ -5,9 +5,10 @@ import { UrlState } from './types';
export const shortenUrl = createAsyncThunk( export const shortenUrl = createAsyncThunk(
'url/shortenUrl', 'url/shortenUrl',
async (longUrl: string) => { async (url: string) => {
const data = await shortenUrlApi({ longUrl }); const baseUrl = process.env.REACT_APP_BASE_URL
return `https://minxa.lol/${data.shortCode}`; const data = await shortenUrlApi({ url });
return `${baseUrl}/${data.shortcode}`;
} }
); );

View File

@@ -11,7 +11,7 @@ import {
const Home: React.FC = () => { const Home: React.FC = () => {
/* component level state */ /* component level state */
const [longUrl, setLongUrl] = useState(''); const [url, setUrl] = useState('');
/* global state */ /* global state */
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -22,8 +22,8 @@ const Home: React.FC = () => {
/* methods */ /* methods */
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && longUrl.trim() !== '') { if (e.key === 'Enter' && url.trim() !== '') {
dispatch(shortenUrl(longUrl)); dispatch(shortenUrl(url));
} }
}; };
@@ -42,9 +42,9 @@ const Home: React.FC = () => {
<input <input
type="text" type="text"
placeholder="Enter your long URL here" placeholder="https://example.com/my-long-url"
value={longUrl} value={url}
onChange={(e) => setLongUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
onKeyDown={handleKeyDown} 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" 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} onClick={handleCopy}
className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600 transition"> className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600 transition">
Copy to Clipboard Copy to Clipboard
</button> </button>
</div> </div>
)} )}
</div> </div>