Adding the form and the api call

This commit is contained in:
2026-03-14 00:40:03 -04:00
parent 6ce0fc0768
commit 6b651d3c4f
9 changed files with 463 additions and 75 deletions

28
frontend/.gitignore vendored
View File

@@ -1,28 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Codetyper.nvim - AI coding partner files
*.coder.*
.coder/

View File

@@ -0,0 +1,27 @@
import axios from 'axios';
import type { GenerateResult } from '@types/qrcode';
export async function generateQr(payload: { text: string; size: number }): Promise<GenerateResult> {
const url = 'http://localhost:5000/api/qrcode/generate';
const resp = await axios.post(url, payload, {
responseType: 'arraybuffer',
validateStatus: (s) => s >= 200 && s < 300,
});
const contentType = (resp.headers['content-type'] || '').toString();
// Try JSON parse in case backend returns { imageUrl }
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 (contentType.startsWith('image/')) {
return { mime: contentType, data: resp.data as ArrayBuffer };
}
return { mime: contentType || 'image/png', data: resp.data as ArrayBuffer };
}

View File

@@ -1,43 +1,40 @@
import React, { useState } from 'react';
import axios from 'axios';
import React 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';
const QRForm: React.FC = () => {
const [text, setText] = useState('');
const [size, setSize] = useState<'150' | '300' | '600'>('300');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [imageUrl, setImageUrl] = useState<string | null>(null);
const { state, dispatch } = useQrStore();
const { text, size, loading } = state;
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError(null);
setImageUrl(null);
dispatch({ type: 'setError', payload: null });
dispatch({ type: 'setImageUrl', payload: null });
if (!text.trim()) {
setError('Please enter a URL or text to encode.');
dispatch({ type: 'setError', payload: 'Please enter a URL or text to encode.' });
return;
}
setLoading(true);
dispatch({ type: 'setLoading', payload: true });
try {
// Call a mock API to simulate server-side generation step.
// Using jsonplaceholder as a harmless mock endpoint.
await axios.post('https://jsonplaceholder.typicode.com/posts', {
payload: text,
size,
});
const res: GenerateResult = await generateQr({ text, size });
// Build a QR image URL using a public QR generator for preview purposes.
// In a real implementation the server would return the final image URL or binary data.
const generatedUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(
text
)}`;
setImageUrl(generatedUrl);
} catch (err) {
setError('Failed to generate QR code. Please try again.');
if ('imageUrl' in res && res.imageUrl) {
dispatch({ type: 'setImageUrl', payload: res.imageUrl });
} else if ('data' in res && res.data && res.mime) {
const blob = new Blob([res.data], { type: res.mime });
const objectUrl = URL.createObjectURL(blob);
dispatch({ type: 'setImageUrl', payload: objectUrl });
} else {
dispatch({ type: 'setError', payload: 'Unexpected server response' });
}
} catch {
dispatch({ type: 'setError', payload: 'Failed to generate QR code. Please try again.' });
} finally {
setLoading(false);
dispatch({ type: 'setLoading', payload: false });
}
};
@@ -48,15 +45,15 @@ const QRForm: React.FC = () => {
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
onChange={(e) => dispatch({ type: 'setText', payload: e.target.value })}
placeholder="https://example.com or some text"
className={styles.qrFormInput}
/>
<label className={styles.qrFormLabel}>Image size</label>
<select
value={size}
onChange={(e) => setSize(e.target.value as '150' | '300' | '600')}
value={String(size)}
onChange={(e) => dispatch({ type: 'setSize', payload: Number(e.target.value) })}
className={styles.qrFormSelect}
>
<option value="150">Small 150x150</option>
@@ -64,7 +61,7 @@ const QRForm: React.FC = () => {
<option value="600">Large 600x600</option>
</select>
{error && <div className={styles.qrError}>{error}</div>}
{state.error && <div className={styles.qrError}>{state.error}</div>}
<button type="submit" disabled={loading} className={styles.qrFormButton}>
{loading ? 'Generating…' : 'Generate'}

View File

@@ -1,23 +1,41 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import styles from '@styles/QRImage.module.scss';
import { useQrStore } from '@store/qrStore';
type Props = {
src?: string;
alt?: string;
filename?: string;
};
const QRImage: React.FC = () => {
const { state } = useQrStore();
const { imageUrl } = state;
const imgRef = useRef<HTMLImageElement | null>(null);
const prevObjectRef = useRef<string | null>(null);
const QRImage = ({ src, alt = 'QR code', filename = 'qrcode.png' }: Props) => {
const imgRef = React.useRef<HTMLImageElement | null>(null);
useEffect(() => {
if (prevObjectRef.current && prevObjectRef.current.startsWith('blob:')) {
try {
URL.revokeObjectURL(prevObjectRef.current);
} catch {
// ignore
}
}
if (imageUrl && imageUrl.startsWith('blob:')) prevObjectRef.current = imageUrl;
else prevObjectRef.current = null;
return () => {
if (prevObjectRef.current && prevObjectRef.current.startsWith('blob:')) {
try {
URL.revokeObjectURL(prevObjectRef.current);
} catch {
// ignore
}
}
};
}, [imageUrl]);
const handleDownload = () => {
const img = imgRef.current;
if (!img || !img.src) {
return;
}
if (!img || !img.src) return;
const link = document.createElement('a');
link.href = img.src;
link.download = filename;
link.download = 'qrcode.png';
document.body.appendChild(link);
link.click();
link.remove();
@@ -26,8 +44,8 @@ const QRImage = ({ src, alt = 'QR code', filename = 'qrcode.png' }: Props) => {
return (
<div className={styles.qrImageBody}>
<div className={styles.qrImageContainer}>
{src ? (
<img ref={imgRef} src={src} alt={alt} className={styles.qrImg} />
{imageUrl ? (
<img ref={imgRef} src={imageUrl} alt="QR code" className={styles.qrImg} />
) : (
<span className={styles.qrSpan}>No image</span>
)}

View File

@@ -2,9 +2,12 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import { QrProvider } from '@src/store/qrStore';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<QrProvider>
<App />
</QrProvider>
</StrictMode>
);

View File

@@ -0,0 +1,56 @@
import React, { createContext, useContext, useReducer } from 'react';
export type QRState = {
text: string;
size: number;
imageUrl: string | null;
loading: boolean;
error: string | null;
};
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,
};
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<Action> } | undefined>(
undefined
);
export const QrProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return <QrContext.Provider value={{ state, dispatch }}>{children}</QrContext.Provider>;
};
export function useQrStore() {
const ctx = useContext(QrContext);
if (!ctx) throw new Error('useQrStore must be used within QrProvider');
return ctx;
}

1
frontend/src/types/qrcode.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export type GenerateResult = { imageUrl?: string } | { mime: string; data: ArrayBuffer };

View File

@@ -6,9 +6,10 @@
"@/*": ["src/*"],
"@src/*": ["src/*"],
"@components/*": ["src/components/*"],
"@api/*": ["src/api/*"],
"@store/*": ["src/store/*"],
"@types/*": ["src/types/*"],
"@styles/*": ["src/styles/*"],
"@atoms/*": ["src/components/atoms/*"],
"@molecules/*": ["src/components/molecules/*"],
"@organisms/*": ["src/components/organisms/*"],
"@templates/*": ["src/components/templates/*"]
}