From 21533ff4f5ee1c31d3c9efaef8bbe8872d201b17 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Sat, 14 Mar 2026 02:50:54 -0400 Subject: [PATCH] adding the logic to generate the image --- backend/app/api/qr.py | 16 ++++++ backend/app/main.py | 24 +++++++++ backend/app/schemas/qr.py | 8 +++ backend/app/services/qr_service.py | 19 ++++++++ backend/main.py | 4 ++ backend/requirements.txt | 5 ++ frontend/src/api/qrcode.ts | 14 +++--- frontend/src/components/organisms/QRForm.tsx | 51 ++++++++++++++++---- frontend/src/constants/index.ts | 24 +++++++++ frontend/src/store/qrStore.tsx | 49 +++++-------------- frontend/src/styles/QRImage.module.scss | 3 -- frontend/src/types/qrcode.d.ts | 20 ++++++++ frontend/tsconfig.json | 3 +- frontend/vite.config.ts | 4 ++ 14 files changed, 188 insertions(+), 56 deletions(-) create mode 100644 backend/app/api/qr.py create mode 100644 backend/app/main.py create mode 100644 backend/app/schemas/qr.py create mode 100644 backend/app/services/qr_service.py create mode 100644 backend/main.py create mode 100644 backend/requirements.txt create mode 100644 frontend/src/constants/index.ts diff --git a/backend/app/api/qr.py b/backend/app/api/qr.py new file mode 100644 index 0000000..38acdf6 --- /dev/null +++ b/backend/app/api/qr.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, HTTPException +from fastapi.responses import Response + +from app.schemas.qr import QRRequest +from app.services.qr_service import generate_qr_image_bytes + +router = APIRouter() + + +@router.post("/generate") +async def generate_qr(payload: QRRequest): + try: + img_bytes = generate_qr_image_bytes(payload.text, payload.size) + return Response(content=img_bytes, media_type="image/png") + except Exception as exc: # pragma: no cover + raise HTTPException(status_code=500, detail=str(exc)) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..d12a489 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.qr import router as qr_router + + +def create_app() -> FastAPI: + app = FastAPI(title="QR Code Service") + + # Open CORS to all origins + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + app.include_router(qr_router, prefix="/api/qrcode") + + return app + + +app = create_app() diff --git a/backend/app/schemas/qr.py b/backend/app/schemas/qr.py new file mode 100644 index 0000000..d5c02e9 --- /dev/null +++ b/backend/app/schemas/qr.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, Field + + +class QRRequest(BaseModel): + text: str = Field(..., description="Text or URL to encode into the QR code") + size: int = Field( + 500, gt=0, le=2000, description="Output image size in pixels (square)" + ) diff --git a/backend/app/services/qr_service.py b/backend/app/services/qr_service.py new file mode 100644 index 0000000..0d1e7c9 --- /dev/null +++ b/backend/app/services/qr_service.py @@ -0,0 +1,19 @@ +from io import BytesIO + +import qrcode + + +def generate_qr_image_bytes(text: str, size: int = 300) -> bytes: + img = qrcode.make(text) + + # Ensure RGB for consistent PNG output + if getattr(img, "mode", None) != "RGB": + img = img.convert("RGB") + + if size: + img = img.resize((size, size)) + + buf = BytesIO() + img.save(buf, "PNG") + buf.seek(0) + return buf.getvalue() diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..8f72773 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.run("app.main:app", host="0.0.0.0", port=5001, log_level="info") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..cdae5d7 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn[standard] +qrcode +pillow +pydantic diff --git a/frontend/src/api/qrcode.ts b/frontend/src/api/qrcode.ts index 40a8b11..ef35944 100644 --- a/frontend/src/api/qrcode.ts +++ b/frontend/src/api/qrcode.ts @@ -1,8 +1,10 @@ import axios from 'axios'; -import type { GenerateResult } from '@types/qrcode'; +import type { GenerateResult } from '@appTypes/qrcode'; + +const host = 'http://localhost:5001'; export async function generateQr(payload: { text: string; size: number }): Promise { - const url = 'http://localhost:5000/api/qrcode/generate'; + const url = `${host}/api/qrcode/generate`; const resp = await axios.post(url, payload, { responseType: 'arraybuffer', validateStatus: (s) => s >= 200 && s < 300, @@ -14,10 +16,10 @@ export async function generateQr(payload: { text: string; size: number }): Promi try { const decoded = new TextDecoder().decode(resp.data); const parsed = JSON.parse(decoded); - if (parsed?.imageUrl) return { imageUrl: parsed.imageUrl }; - } catch (error) { - console.error(error); - } + if (parsed?.imageUrl) { + return { imageUrl: parsed.imageUrl }; + } + } catch {} if (contentType.startsWith('image/')) { return { mime: contentType, data: resp.data as ArrayBuffer }; diff --git a/frontend/src/components/organisms/QRForm.tsx b/frontend/src/components/organisms/QRForm.tsx index ff232d4..1a3cc9e 100644 --- a/frontend/src/components/organisms/QRForm.tsx +++ b/frontend/src/components/organisms/QRForm.tsx @@ -1,15 +1,15 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import styles from '@styles/QRForm.module.scss'; import { useQrStore } from '@store/qrStore'; import { generateQr } from '@api/qrcode'; -import type { GenerateResult } from '@types/qrcode'; +import { imageSizes, initialState } from '@/constants'; +import type { GenerateResult } from '@appTypes/qrcode'; const QRForm: React.FC = () => { const { state, dispatch } = useQrStore(); const { text, size, loading } = state; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const callingApi = async () => { dispatch({ type: 'setError', payload: null }); dispatch({ type: 'setImageUrl', payload: null }); @@ -38,15 +38,39 @@ const QRForm: React.FC = () => { } }; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await callingApi(); + }; + + const handleReset = () => { + dispatch({ type: 'restore', payload: null }); + }; + + const hasCalledRef = useRef(false); + useEffect(() => { + if (hasCalledRef.current) { + return; + } + hasCalledRef.current = true; + callingApi(); + }, []); + + useEffect(() => { + if (!text) { + dispatch({ type: 'restore', payload: null }); + } + }, [text]); + return (
-
+ dispatch({ type: 'setText', payload: e.target.value })} - placeholder="https://example.com or some text" + placeholder={text} className={styles.qrFormInput} /> @@ -56,9 +80,13 @@ const QRForm: React.FC = () => { onChange={(e) => dispatch({ type: 'setSize', payload: Number(e.target.value) })} className={styles.qrFormSelect} > - - - + {imageSizes.map((imageSize) => { + return ( + + ); + })} {state.error &&
{state.error}
} @@ -66,6 +94,9 @@ const QRForm: React.FC = () => { +
); diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts new file mode 100644 index 0000000..547a8a0 --- /dev/null +++ b/frontend/src/constants/index.ts @@ -0,0 +1,24 @@ +import type { QRState, ImageSizes } from '@appTypes/'; + +export const initialState: QRState = { + text: 'https://www.bioxsystems.com/', + size: 600, + imageUrl: null, + loading: false, + error: null, +}; + +export const imageSizes: ImageSizes[] = [ + { + value: 150, + text: 'Small — 150x150', + }, + { + value: 300, + text: 'Medium — 300x300', + }, + { + value: 600, + text: 'Large — 600x600', + }, +]; diff --git a/frontend/src/store/qrStore.tsx b/frontend/src/store/qrStore.tsx index 1a4fda4..1448e0e 100644 --- a/frontend/src/store/qrStore.tsx +++ b/frontend/src/store/qrStore.tsx @@ -1,45 +1,22 @@ import React, { createContext, useContext, useReducer } from 'react'; +import type { QRState } from '@appTypes/'; +import { initialState } from '@/constants'; -export type QRState = { - text: string; - size: number; - imageUrl: string | null; - loading: boolean; - error: string | null; +const handlers: Record QRState> = { + setText: (state, payload) => ({ ...state, text: payload }), + setSize: (state, payload) => ({ ...state, size: payload }), + setImageUrl: (state, payload) => ({ ...state, imageUrl: payload }), + setLoading: (state, payload) => ({ ...state, loading: payload }), + setError: (state, payload) => ({ ...state, error: payload }), + restore: (state) => ({ ...state, text: initialState.text }), }; -type Action = - | { type: 'setText'; payload: string } - | { type: 'setSize'; payload: number } - | { type: 'setImageUrl'; payload: string | null } - | { type: 'setLoading'; payload: boolean } - | { type: 'setError'; payload: string | null }; - -const initialState: QRState = { - text: '', - size: 300, - imageUrl: null, - loading: false, - error: null, +const reducer = (state: QRState, action: Action): QRState => { + const handler = (handlers as any)[action.type]; + if (handler) return handler(state, (action as any).payload); + return state; }; -function reducer(state: QRState, action: Action): QRState { - switch (action.type) { - case 'setText': - return { ...state, text: action.payload }; - case 'setSize': - return { ...state, size: action.payload }; - case 'setImageUrl': - return { ...state, imageUrl: action.payload }; - case 'setLoading': - return { ...state, loading: action.payload }; - case 'setError': - return { ...state, error: action.payload }; - default: - return state; - } -} - const QrContext = createContext<{ state: QRState; dispatch: React.Dispatch } | undefined>( undefined ); diff --git a/frontend/src/styles/QRImage.module.scss b/frontend/src/styles/QRImage.module.scss index ad9c651..c9a4345 100644 --- a/frontend/src/styles/QRImage.module.scss +++ b/frontend/src/styles/QRImage.module.scss @@ -1,13 +1,10 @@ .qrImageContainer { - width: 320px; - height: 320px; display: flex; align-items: center; justify-content: center; overflow: hidden; background: linear-gradient(180deg, #ffffff 0%, #ffffff 100%); border-radius: 18px; - padding: 18px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45); border: 6px solid #ffffff; /* white frame like design */ } diff --git a/frontend/src/types/qrcode.d.ts b/frontend/src/types/qrcode.d.ts index 7771c86..172e8dc 100644 --- a/frontend/src/types/qrcode.d.ts +++ b/frontend/src/types/qrcode.d.ts @@ -1 +1,21 @@ export type GenerateResult = { imageUrl?: string } | { mime: string; data: ArrayBuffer }; + +export type QRState = { + text: string; + size: number; + imageUrl: string | null; + loading: boolean; + error: string | null; +}; + +export type Action = + | { type: 'setText'; payload: string } + | { type: 'setSize'; payload: number } + | { type: 'setImageUrl'; payload: string | null } + | { type: 'setLoading'; payload: boolean } + | { type: 'setError'; payload: string | null }; + +export type ImageSizes = { + value: number; + text: string; +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 412f034..b7aae94 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -8,8 +8,9 @@ "@components/*": ["src/components/*"], "@api/*": ["src/api/*"], "@store/*": ["src/store/*"], - "@types/*": ["src/types/*"], + "@appTypes/*": ["src/types/*"], "@styles/*": ["src/styles/*"], + "@constants/*": ["src/constants/*"], "@organisms/*": ["src/components/organisms/*"], "@templates/*": ["src/components/templates/*"] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 628134b..eb3d5ad 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -17,8 +17,12 @@ export default defineConfig({ '@templates': '/src/components/templates', '@atoms': '/src/components/atoms', '@molecules': '/src/components/molecules', + '@constants': ['src/constants'], '@organisms': '/src/components/organisms', '@styles': '/src/styles', + '@api': '/src/api', + '@store': '/src/store', + '@appTypes': '/src/types', }, }, });