adding the logic to generate the image

This commit is contained in:
2026-03-14 02:50:54 -04:00
parent 6b651d3c4f
commit 21533ff4f5
14 changed files with 188 additions and 56 deletions

16
backend/app/api/qr.py Normal file
View File

@@ -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))

24
backend/app/main.py Normal file
View File

@@ -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()

View File

@@ -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)"
)

View File

@@ -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()

4
backend/main.py Normal file
View File

@@ -0,0 +1,4 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.main:app", host="0.0.0.0", port=5001, log_level="info")

5
backend/requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
fastapi
uvicorn[standard]
qrcode
pillow
pydantic

View File

@@ -1,8 +1,10 @@
import axios from 'axios'; 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<GenerateResult> { export async function generateQr(payload: { text: string; size: number }): Promise<GenerateResult> {
const url = 'http://localhost:5000/api/qrcode/generate'; const url = `${host}/api/qrcode/generate`;
const resp = await axios.post(url, payload, { const resp = await axios.post(url, payload, {
responseType: 'arraybuffer', responseType: 'arraybuffer',
validateStatus: (s) => s >= 200 && s < 300, validateStatus: (s) => s >= 200 && s < 300,
@@ -14,10 +16,10 @@ export async function generateQr(payload: { text: string; size: number }): Promi
try { try {
const decoded = new TextDecoder().decode(resp.data); const decoded = new TextDecoder().decode(resp.data);
const parsed = JSON.parse(decoded); const parsed = JSON.parse(decoded);
if (parsed?.imageUrl) return { imageUrl: parsed.imageUrl }; if (parsed?.imageUrl) {
} catch (error) { return { imageUrl: parsed.imageUrl };
console.error(error);
} }
} catch {}
if (contentType.startsWith('image/')) { if (contentType.startsWith('image/')) {
return { mime: contentType, data: resp.data as ArrayBuffer }; return { mime: contentType, data: resp.data as ArrayBuffer };

View File

@@ -1,15 +1,15 @@
import React from 'react'; import React, { useEffect, useRef } from 'react';
import styles from '@styles/QRForm.module.scss'; import styles from '@styles/QRForm.module.scss';
import { useQrStore } from '@store/qrStore'; import { useQrStore } from '@store/qrStore';
import { generateQr } from '@api/qrcode'; 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 QRForm: React.FC = () => {
const { state, dispatch } = useQrStore(); const { state, dispatch } = useQrStore();
const { text, size, loading } = state; const { text, size, loading } = state;
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const callingApi = async () => {
e.preventDefault();
dispatch({ type: 'setError', payload: null }); dispatch({ type: 'setError', payload: null });
dispatch({ type: 'setImageUrl', payload: null }); dispatch({ type: 'setImageUrl', payload: null });
@@ -38,15 +38,39 @@ const QRForm: React.FC = () => {
} }
}; };
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
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 ( return (
<div className={styles.qrFormBody}> <div className={styles.qrFormBody}>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit} onReset={handleReset}>
<label className={styles.qrFormLabel}>Submit URL or text</label> <label className={styles.qrFormLabel}>Submit URL or text</label>
<input <input
type="text" type="text"
value={text} value={text === initialState.text ? null : text}
onChange={(e) => dispatch({ type: 'setText', payload: e.target.value })} onChange={(e) => dispatch({ type: 'setText', payload: e.target.value })}
placeholder="https://example.com or some text" placeholder={text}
className={styles.qrFormInput} className={styles.qrFormInput}
/> />
@@ -56,9 +80,13 @@ const QRForm: React.FC = () => {
onChange={(e) => dispatch({ type: 'setSize', payload: Number(e.target.value) })} onChange={(e) => dispatch({ type: 'setSize', payload: Number(e.target.value) })}
className={styles.qrFormSelect} className={styles.qrFormSelect}
> >
<option value="150">Small 150x150</option> {imageSizes.map((imageSize) => {
<option value="300">Medium 300x300</option> return (
<option value="600">Large 600x600</option> <option key={imageSize.value} value={imageSize.value}>
{imageSize.text}
</option>
);
})}
</select> </select>
{state.error && <div className={styles.qrError}>{state.error}</div>} {state.error && <div className={styles.qrError}>{state.error}</div>}
@@ -66,6 +94,9 @@ const QRForm: React.FC = () => {
<button type="submit" disabled={loading} className={styles.qrFormButton}> <button type="submit" disabled={loading} className={styles.qrFormButton}>
{loading ? 'Generating…' : 'Generate'} {loading ? 'Generating…' : 'Generate'}
</button> </button>
<button type="reset" className={styles.qrFormButton}>
reset
</button>
</form> </form>
</div> </div>
); );

View File

@@ -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',
},
];

View File

@@ -1,44 +1,21 @@
import React, { createContext, useContext, useReducer } from 'react'; import React, { createContext, useContext, useReducer } from 'react';
import type { QRState } from '@appTypes/';
import { initialState } from '@/constants';
export type QRState = { const handlers: Record<string, (state: QRState, payload?: any) => QRState> = {
text: string; setText: (state, payload) => ({ ...state, text: payload }),
size: number; setSize: (state, payload) => ({ ...state, size: payload }),
imageUrl: string | null; setImageUrl: (state, payload) => ({ ...state, imageUrl: payload }),
loading: boolean; setLoading: (state, payload) => ({ ...state, loading: payload }),
error: string | null; setError: (state, payload) => ({ ...state, error: payload }),
restore: (state) => ({ ...state, text: initialState.text }),
}; };
type Action = const reducer = (state: QRState, action: Action): QRState => {
| { type: 'setText'; payload: string } const handler = (handlers as any)[action.type];
| { type: 'setSize'; payload: number } if (handler) return handler(state, (action as any).payload);
| { 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,
};
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; return state;
} };
}
const QrContext = createContext<{ state: QRState; dispatch: React.Dispatch<Action> } | undefined>( const QrContext = createContext<{ state: QRState; dispatch: React.Dispatch<Action> } | undefined>(
undefined undefined

View File

@@ -1,13 +1,10 @@
.qrImageContainer { .qrImageContainer {
width: 320px;
height: 320px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
background: linear-gradient(180deg, #ffffff 0%, #ffffff 100%); background: linear-gradient(180deg, #ffffff 0%, #ffffff 100%);
border-radius: 18px; border-radius: 18px;
padding: 18px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
border: 6px solid #ffffff; /* white frame like design */ border: 6px solid #ffffff; /* white frame like design */
} }

View File

@@ -1 +1,21 @@
export type GenerateResult = { imageUrl?: string } | { mime: string; data: ArrayBuffer }; 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;
};

View File

@@ -8,8 +8,9 @@
"@components/*": ["src/components/*"], "@components/*": ["src/components/*"],
"@api/*": ["src/api/*"], "@api/*": ["src/api/*"],
"@store/*": ["src/store/*"], "@store/*": ["src/store/*"],
"@types/*": ["src/types/*"], "@appTypes/*": ["src/types/*"],
"@styles/*": ["src/styles/*"], "@styles/*": ["src/styles/*"],
"@constants/*": ["src/constants/*"],
"@organisms/*": ["src/components/organisms/*"], "@organisms/*": ["src/components/organisms/*"],
"@templates/*": ["src/components/templates/*"] "@templates/*": ["src/components/templates/*"]
} }

View File

@@ -17,8 +17,12 @@ export default defineConfig({
'@templates': '/src/components/templates', '@templates': '/src/components/templates',
'@atoms': '/src/components/atoms', '@atoms': '/src/components/atoms',
'@molecules': '/src/components/molecules', '@molecules': '/src/components/molecules',
'@constants': ['src/constants'],
'@organisms': '/src/components/organisms', '@organisms': '/src/components/organisms',
'@styles': '/src/styles', '@styles': '/src/styles',
'@api': '/src/api',
'@store': '/src/store',
'@appTypes': '/src/types',
}, },
}, },
}); });