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

313
.gitignore vendored Normal file
View File

@@ -0,0 +1,313 @@
# Created by https://www.toptal.com/developers/gitignore/api/node,python
# Edit at https://www.toptal.com/developers/gitignore?templates=node,python
.coder
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/node,python

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/*"]
}