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 React from 'react';
|
||||||
import axios from 'axios';
|
|
||||||
import styles from '@styles/QRForm.module.scss';
|
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 QRForm: React.FC = () => {
|
||||||
const [text, setText] = useState('');
|
const { state, dispatch } = useQrStore();
|
||||||
const [size, setSize] = useState<'150' | '300' | '600'>('300');
|
const { text, size, loading } = state;
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
dispatch({ type: 'setError', payload: null });
|
||||||
setImageUrl(null);
|
dispatch({ type: 'setImageUrl', payload: null });
|
||||||
|
|
||||||
if (!text.trim()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
dispatch({ type: 'setLoading', payload: true });
|
||||||
try {
|
try {
|
||||||
// Call a mock API to simulate server-side generation step.
|
const res: GenerateResult = await generateQr({ text, size });
|
||||||
// Using jsonplaceholder as a harmless mock endpoint.
|
|
||||||
await axios.post('https://jsonplaceholder.typicode.com/posts', {
|
|
||||||
payload: text,
|
|
||||||
size,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build a QR image URL using a public QR generator for preview purposes.
|
if ('imageUrl' in res && res.imageUrl) {
|
||||||
// In a real implementation the server would return the final image URL or binary data.
|
dispatch({ type: 'setImageUrl', payload: res.imageUrl });
|
||||||
const generatedUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(
|
} else if ('data' in res && res.data && res.mime) {
|
||||||
text
|
const blob = new Blob([res.data], { type: res.mime });
|
||||||
)}`;
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
setImageUrl(generatedUrl);
|
dispatch({ type: 'setImageUrl', payload: objectUrl });
|
||||||
} catch (err) {
|
} else {
|
||||||
setError('Failed to generate QR code. Please try again.');
|
dispatch({ type: 'setError', payload: 'Unexpected server response' });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
dispatch({ type: 'setError', payload: 'Failed to generate QR code. Please try again.' });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
dispatch({ type: 'setLoading', payload: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,15 +45,15 @@ const QRForm: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={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"
|
placeholder="https://example.com or some text"
|
||||||
className={styles.qrFormInput}
|
className={styles.qrFormInput}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label className={styles.qrFormLabel}>Image size</label>
|
<label className={styles.qrFormLabel}>Image size</label>
|
||||||
<select
|
<select
|
||||||
value={size}
|
value={String(size)}
|
||||||
onChange={(e) => setSize(e.target.value as '150' | '300' | '600')}
|
onChange={(e) => dispatch({ type: 'setSize', payload: Number(e.target.value) })}
|
||||||
className={styles.qrFormSelect}
|
className={styles.qrFormSelect}
|
||||||
>
|
>
|
||||||
<option value="150">Small — 150x150</option>
|
<option value="150">Small — 150x150</option>
|
||||||
@@ -64,7 +61,7 @@ const QRForm: React.FC = () => {
|
|||||||
<option value="600">Large — 600x600</option>
|
<option value="600">Large — 600x600</option>
|
||||||
</select>
|
</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}>
|
<button type="submit" disabled={loading} className={styles.qrFormButton}>
|
||||||
{loading ? 'Generating…' : 'Generate'}
|
{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 styles from '@styles/QRImage.module.scss';
|
||||||
|
import { useQrStore } from '@store/qrStore';
|
||||||
|
|
||||||
type Props = {
|
const QRImage: React.FC = () => {
|
||||||
src?: string;
|
const { state } = useQrStore();
|
||||||
alt?: string;
|
const { imageUrl } = state;
|
||||||
filename?: string;
|
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||||
};
|
const prevObjectRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const QRImage = ({ src, alt = 'QR code', filename = 'qrcode.png' }: Props) => {
|
useEffect(() => {
|
||||||
const imgRef = React.useRef<HTMLImageElement | null>(null);
|
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 handleDownload = () => {
|
||||||
const img = imgRef.current;
|
const img = imgRef.current;
|
||||||
if (!img || !img.src) {
|
if (!img || !img.src) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = img.src;
|
link.href = img.src;
|
||||||
link.download = filename;
|
link.download = 'qrcode.png';
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
@@ -26,8 +44,8 @@ const QRImage = ({ src, alt = 'QR code', filename = 'qrcode.png' }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.qrImageBody}>
|
<div className={styles.qrImageBody}>
|
||||||
<div className={styles.qrImageContainer}>
|
<div className={styles.qrImageContainer}>
|
||||||
{src ? (
|
{imageUrl ? (
|
||||||
<img ref={imgRef} src={src} alt={alt} className={styles.qrImg} />
|
<img ref={imgRef} src={imageUrl} alt="QR code" className={styles.qrImg} />
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.qrSpan}>No image</span>
|
<span className={styles.qrSpan}>No image</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { StrictMode } from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
|
import { QrProvider } from '@src/store/qrStore';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<QrProvider>
|
||||||
|
<App />
|
||||||
|
</QrProvider>
|
||||||
</StrictMode>
|
</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/*": ["src/*"],
|
"@src/*": ["src/*"],
|
||||||
"@components/*": ["src/components/*"],
|
"@components/*": ["src/components/*"],
|
||||||
|
"@api/*": ["src/api/*"],
|
||||||
|
"@store/*": ["src/store/*"],
|
||||||
|
"@types/*": ["src/types/*"],
|
||||||
"@styles/*": ["src/styles/*"],
|
"@styles/*": ["src/styles/*"],
|
||||||
"@atoms/*": ["src/components/atoms/*"],
|
|
||||||
"@molecules/*": ["src/components/molecules/*"],
|
|
||||||
"@organisms/*": ["src/components/organisms/*"],
|
"@organisms/*": ["src/components/organisms/*"],
|
||||||
"@templates/*": ["src/components/templates/*"]
|
"@templates/*": ["src/components/templates/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user