feat: connect backend with the frontend
This commit is contained in:
@@ -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
|
||||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
4
frontend/.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Frontend environment variables #
|
||||||
|
|
||||||
|
REACT_APP_API_BASE_URL=http://localhost:8000
|
||||||
|
REACT_APP_BASE_URL=http://localhost:8000
|
||||||
1
frontend/package-lock.json
generated
1
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user