Adding the form and the api call
This commit is contained in:
28
frontend/.gitignore
vendored
28
frontend/.gitignore
vendored
@@ -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/
|
||||
27
frontend/src/api/qrcode.ts
Normal file
27
frontend/src/api/qrcode.ts
Normal 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 };
|
||||
}
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
56
frontend/src/store/qrStore.tsx
Normal file
56
frontend/src/store/qrStore.tsx
Normal 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
1
frontend/src/types/qrcode.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export type GenerateResult = { imageUrl?: string } | { mime: string; data: ArrayBuffer };
|
||||
@@ -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/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user