Adding the form and the api call
This commit is contained in:
313
.gitignore
vendored
Normal file
313
.gitignore
vendored
Normal 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
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