adding the logic to generate the image
This commit is contained in:
16
backend/app/api/qr.py
Normal file
16
backend/app/api/qr.py
Normal 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
24
backend/app/main.py
Normal 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()
|
||||||
8
backend/app/schemas/qr.py
Normal file
8
backend/app/schemas/qr.py
Normal 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)"
|
||||||
|
)
|
||||||
19
backend/app/services/qr_service.py
Normal file
19
backend/app/services/qr_service.py
Normal 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
4
backend/main.py
Normal 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
5
backend/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
qrcode
|
||||||
|
pillow
|
||||||
|
pydantic
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
24
frontend/src/constants/index.ts
Normal file
24
frontend/src/constants/index.ts
Normal 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',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 */
|
||||||
}
|
}
|
||||||
|
|||||||
20
frontend/src/types/qrcode.d.ts
vendored
20
frontend/src/types/qrcode.d.ts
vendored
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user