Initial commit: any%
This commit is contained in:
		
						commit
						c938303a07
					
				
					 13 changed files with 4313 additions and 0 deletions
				
			
		
							
								
								
									
										3
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| { | ||||
|   "extends": "next/core-web-vitals" | ||||
| } | ||||
							
								
								
									
										36
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||||
| 
 | ||||
| # dependencies | ||||
| /node_modules | ||||
| /.pnp | ||||
| .pnp.js | ||||
| 
 | ||||
| # testing | ||||
| /coverage | ||||
| 
 | ||||
| # next.js | ||||
| /.next/ | ||||
| /out/ | ||||
| 
 | ||||
| # production | ||||
| /build | ||||
| 
 | ||||
| # misc | ||||
| .DS_Store | ||||
| *.pem | ||||
| 
 | ||||
| # debug | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| .pnpm-debug.log* | ||||
| 
 | ||||
| # local env files | ||||
| .env*.local | ||||
| 
 | ||||
| # vercel | ||||
| .vercel | ||||
| 
 | ||||
| # typescript | ||||
| *.tsbuildinfo | ||||
| next-env.d.ts | ||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| # qr.umm.gay | ||||
| 
 | ||||
| qr decoder web tool any% (20 minutes) | ||||
| 
 | ||||
| ## Setup | ||||
| 
 | ||||
| ```shell | ||||
| $ npm install # Install dependencies | ||||
| $ npm run dev # Start a local dev server | ||||
| $ npm run build # Generate static bundle in 'out/' directory | ||||
| ``` | ||||
							
								
								
									
										155
									
								
								lib/gluejar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								lib/gluejar.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,155 @@ | |||
| import { useEffect, useCallback, useReducer } from "react"; | ||||
| 
 | ||||
| /* <PasteContainer | ||||
|   onPaste={files => files.length > 0 && this.props.onFileAccepted(files[0], files[0])} | ||||
| /> */ | ||||
| 
 | ||||
| // NOTE: `onPaste` should return an array of files that were acceptedFiles
 | ||||
| //
 | ||||
| // const Image = ({ src }) => <img src={image} alt={`Pasted: ${image}`} />
 | ||||
| //
 | ||||
| // <PasteContainer
 | ||||
| //    onPaste={files => this.method(files)}
 | ||||
| //    onError={err => console.error(err)}
 | ||||
| // >
 | ||||
| //   {({images}) => images.map((image, i) => <Image src={image} key={i} />)}
 | ||||
| // </PasteContainer>
 | ||||
| 
 | ||||
| const DEFAULT_ACCEPTED_FILES = ["image/gif", "image/png", "image/jpeg", "image/bmp"]; | ||||
| 
 | ||||
| const _transformImageDataToURL = ( | ||||
| 	data: DataTransfer, | ||||
| 	acceptedFiles: string[] = DEFAULT_ACCEPTED_FILES | ||||
| ): string | undefined => { | ||||
| 	const isValidFormat = (fileType: string): boolean => acceptedFiles.includes(fileType); | ||||
| 	// NOTE: This needs to be a for loop, it's a list like object
 | ||||
| 	for (let i = 0; i < data.items.length; i++) { | ||||
| 		if (!isValidFormat(data.items[i].type)) { | ||||
| 			continue; | ||||
| 			// throw new Error(`Sorry, that's not a format we support ${data.items[i].type}`);
 | ||||
| 		} | ||||
| 		let blob: BlobLikeFile = data.items[i].getAsFile(); | ||||
| 
 | ||||
| 		if (blob) { | ||||
| 			// We shouldn't fire the callback if we can't create `new Blob()`
 | ||||
| 			let file = window.URL.createObjectURL(blob); | ||||
| 
 | ||||
| 			return file; | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| type BlobLikeFile = File | null; | ||||
| 
 | ||||
| export interface GlueJarOptions<T extends HTMLElement> { | ||||
| 	acceptedFiles: string[]; | ||||
| 	ref?: React.RefObject<T>; | ||||
| } | ||||
| 
 | ||||
| export interface IGlueJarState { | ||||
| 	pasted: string[]; | ||||
| 	error: string | null; | ||||
| } | ||||
| 
 | ||||
| export enum GlueActions { | ||||
| 	SET_FILE = "SET_FILES", | ||||
| 	SET_ERROR = "SET_ERROR", | ||||
| } | ||||
| 
 | ||||
| type GlueJarActions = | ||||
| 	| { | ||||
| 			type: GlueActions.SET_FILE; | ||||
| 			file: string; | ||||
| 	  } | ||||
| 	| { type: GlueActions.SET_ERROR; error: string }; | ||||
| 
 | ||||
| type GlueJarReducer = React.Reducer<IGlueJarState, GlueJarActions>; | ||||
| 
 | ||||
| const reducer: GlueJarReducer = (state, action) => { | ||||
| 	switch (action.type) { | ||||
| 		case GlueActions.SET_FILE: | ||||
| 			return { ...state, pasted: [action.file, ...state.pasted], error: null }; | ||||
| 		case GlueActions.SET_ERROR: | ||||
| 			return { ...state, error: action.error }; | ||||
| 		default: | ||||
| 			throw new Error("Must specify action type"); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| const useGlueJarReducer = () => | ||||
| 	useReducer<GlueJarReducer>(reducer, { | ||||
| 		pasted: [], | ||||
| 		error: null, | ||||
| 	}); | ||||
| 
 | ||||
| export function usePasteHandler(acceptedFiles: string[] = DEFAULT_ACCEPTED_FILES) { | ||||
| 	const [state, dispatch] = useGlueJarReducer(); | ||||
| 
 | ||||
| 	const transformImageDataToURL = useCallback( | ||||
| 		(data: DataTransfer): void => { | ||||
| 			try { | ||||
| 				const file = _transformImageDataToURL(data, acceptedFiles); | ||||
| 				dispatch( | ||||
| 					file | ||||
| 						? { type: GlueActions.SET_FILE, file } | ||||
| 						: { | ||||
| 								type: GlueActions.SET_ERROR, | ||||
| 								error: "Something went wrong", | ||||
| 						  } | ||||
| 				); | ||||
| 			} catch (error: any) { | ||||
| 				dispatch({ type: GlueActions.SET_ERROR, error: error.message }); | ||||
| 			} | ||||
| 		}, | ||||
| 		[dispatch, acceptedFiles] | ||||
| 	); | ||||
| 
 | ||||
| 	const pasteHandler = useCallback( | ||||
| 		({ clipboardData }: ClipboardEvent): void => { | ||||
| 			if (clipboardData && clipboardData.items.length > 0) { | ||||
| 				transformImageDataToURL(clipboardData); | ||||
| 			} else { | ||||
| 				dispatch({ | ||||
| 					type: GlueActions.SET_ERROR, | ||||
| 					error: `Sorry, to bother you but there was no image pasted.`, | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 		[dispatch, transformImageDataToURL] | ||||
| 	); | ||||
| 
 | ||||
| 	return [state, pasteHandler] as const; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * useGlueJar | ||||
|  * if you don't pass a `ref` to the options it will default to use the document | ||||
|  * to add an event listener | ||||
|  * @param options | ||||
|  * @returns `IGlueJarState` | ||||
|  */ | ||||
| export function useGlueJar<T extends HTMLElement>( | ||||
| 	{ ref, ...options }: Partial<GlueJarOptions<T>> = { | ||||
| 		acceptedFiles: DEFAULT_ACCEPTED_FILES, | ||||
| 	} | ||||
| ) { | ||||
| 	const [state, pasteHandler] = usePasteHandler(options.acceptedFiles); | ||||
| 
 | ||||
| 	useEffect(() => { | ||||
| 		if (ref && ref.current) { | ||||
| 			ref.current.addEventListener("paste", pasteHandler); | ||||
| 		} else { | ||||
| 			document.addEventListener("paste", pasteHandler); | ||||
| 		} | ||||
| 
 | ||||
| 		return () => { | ||||
| 			if (ref && ref.current) { | ||||
| 				ref.current.removeEventListener("paste", pasteHandler); | ||||
| 			} else { | ||||
| 				document.removeEventListener("paste", pasteHandler); | ||||
| 			} | ||||
| 		}; | ||||
| 	}, [ref, options]); | ||||
| 
 | ||||
| 	return state; | ||||
| } | ||||
							
								
								
									
										344
									
								
								lib/qr.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								lib/qr.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,344 @@ | |||
| import { Options } from 'jsqr'; | ||||
| import jsQR from 'jsqr'; | ||||
| 
 | ||||
| const videoSize = { | ||||
|   width: { min: 360, ideal: 720, max: 1080 }, | ||||
|   height: { min: 360, ideal: 720, max: 1080 }, | ||||
| }; | ||||
| 
 | ||||
| class QrcodeDecoder { | ||||
|   timerCapture: null | NodeJS.Timeout; | ||||
|   canvasElem: null | HTMLCanvasElement; | ||||
|   gCtx: null | CanvasRenderingContext2D; | ||||
|   stream: null | MediaStream; | ||||
|   videoElem: null | HTMLVideoElement; | ||||
|   getUserMediaHandler: null; | ||||
|   videoConstraints: MediaStreamConstraints; | ||||
|   defaultOption: Options; | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.timerCapture = null; | ||||
|     this.canvasElem = null; | ||||
|     this.gCtx = null; | ||||
|     this.stream = null; | ||||
|     this.videoElem = null; | ||||
|     this.getUserMediaHandler = null; | ||||
|     this.videoConstraints = { | ||||
|       // default use rear camera
 | ||||
|       video: { ...videoSize, facingMode: { exact: 'environment' } }, | ||||
|       audio: false, | ||||
|     }; | ||||
| 
 | ||||
|     this.defaultOption = { inversionAttempts: 'attemptBoth' }; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Verifies if canvas element is supported. | ||||
|    */ | ||||
|   isCanvasSupported() { | ||||
|     const elem = document.createElement('canvas'); | ||||
|     return !!(elem.getContext && elem.getContext('2d')); | ||||
|   } | ||||
| 
 | ||||
|   _createImageData(target: CanvasImageSource, width: number, height: number) { | ||||
|     if (!this.canvasElem) { | ||||
|       this._prepareCanvas(width, height); | ||||
|     } | ||||
| 
 | ||||
|     this.gCtx!.clearRect(0, 0, width, height); | ||||
|     this.gCtx!.drawImage(target, 0, 0, width, height); | ||||
| 
 | ||||
|     const imageData = this.gCtx!.getImageData( | ||||
|       0, | ||||
|       0, | ||||
|       this.canvasElem!.width, | ||||
|       this.canvasElem!.height, | ||||
|     ); | ||||
| 
 | ||||
|     return imageData; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Prepares the canvas element (which will | ||||
|    * receive the image from the camera and provide | ||||
|    * what the algorithm needs for checking for a | ||||
|    * QRCode and then decoding it.) | ||||
|    * | ||||
|    * | ||||
|    * @param  {DOMElement} canvasElem the canvas | ||||
|    *                                 element | ||||
|    * @param  {number} width      The width that | ||||
|    *                             the canvas element | ||||
|    *                             should have | ||||
|    * @param  {number} height     The height that | ||||
|    *                             the canvas element | ||||
|    *                             should have | ||||
|    * @return {DOMElement}            the canvas | ||||
|    * after the resize if width and height | ||||
|    * provided. | ||||
|    */ | ||||
|   _prepareCanvas(width: number, height: number) { | ||||
|     if (!this.canvasElem) { | ||||
|       this.canvasElem = document.createElement('canvas'); | ||||
|       this.canvasElem.style.width = `${width}px`; | ||||
|       this.canvasElem.style.height = `${height}px`; | ||||
|       this.canvasElem.width = width; | ||||
|       this.canvasElem.height = height; | ||||
|     } | ||||
| 
 | ||||
|     this.gCtx = this.canvasElem.getContext('2d'); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Based on the video dimensions and the canvas | ||||
|    * that was previously generated captures the | ||||
|    * video/image source and then paints into the | ||||
|    * canvas so that the decoder is able to work as | ||||
|    * it expects. | ||||
|    * @param  {DOMElement} videoElem <video> dom element | ||||
|    * @param  {Object} options     options (optional) - Additional options. | ||||
|    *  inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst) | ||||
|    *  refer to jsqr options: https://github.com/cozmo/jsQR
 | ||||
|    */ | ||||
|   async _captureToCanvas(videoElem: HTMLVideoElement, options: Options) { | ||||
|     if (this.timerCapture) { | ||||
|       clearTimeout(this.timerCapture); | ||||
|     } | ||||
|     const proms = () => { | ||||
|       const p = new Promise(async (resolve) => { | ||||
|         let code; | ||||
|         if (videoElem.videoWidth && videoElem.videoHeight) { | ||||
|           const imageData = this._createImageData( | ||||
|             videoElem, | ||||
|             videoElem.videoWidth, | ||||
|             videoElem.videoHeight, | ||||
|           ); | ||||
| 
 | ||||
|           code = jsQR( | ||||
|             imageData.data, | ||||
|             imageData.width, | ||||
|             imageData.height, | ||||
|             options, | ||||
|           ); | ||||
| 
 | ||||
|           if (code) { | ||||
|             resolve(code); | ||||
|           } else { | ||||
|             this.timerCapture = setTimeout(async () => { | ||||
|               code = await this._captureToCanvas(videoElem, options); | ||||
|               resolve(code); | ||||
|             }, 500); | ||||
|           } | ||||
|         } else { | ||||
|           this.timerCapture = setTimeout(async () => { | ||||
|             code = await this._captureToCanvas(videoElem, options); | ||||
|             resolve(code); | ||||
|           }, 500); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       return p; | ||||
|     }; | ||||
| 
 | ||||
|     const result = await proms(); | ||||
| 
 | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Prepares the video element for receiving | ||||
|    * camera's input. Releases a stream if there | ||||
|    * was any (resets). | ||||
|    * | ||||
|    * @param  {DOMElement} videoElem <video> dom element | ||||
|    * @param  {Object} options     options (optional) - Additional options. | ||||
|    *  inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst) | ||||
|    *  refer to jsqr options: https://github.com/cozmo/jsQR
 | ||||
|    */ | ||||
|   async decodeFromCamera(videoElem: HTMLVideoElement, options: any = {}) { | ||||
|     const opts = { | ||||
|       ...this.defaultOption, | ||||
|       ...options, | ||||
|     }; | ||||
| 
 | ||||
|     this.stop(); | ||||
|     if (!navigator.mediaDevices.getUserMedia) { | ||||
|       throw new Error("Couldn't get video from camera"); | ||||
|     } | ||||
| 
 | ||||
|     let stream: MediaStream; | ||||
|     try { | ||||
|       stream = await navigator.mediaDevices.getUserMedia(this.videoConstraints); | ||||
|     } catch (e) { | ||||
|       if ((e as OverconstrainedError).name === 'OverconstrainedError') { | ||||
|         console.log('[OverconstrainedError] Can not use rear camera.'); | ||||
| 
 | ||||
|         stream = await navigator.mediaDevices.getUserMedia({ | ||||
|           video: { | ||||
|             ...videoSize, | ||||
|             ...{ | ||||
|               width: opts.width, | ||||
|               height: opts.height, | ||||
|             }, | ||||
|           }, | ||||
|           audio: false, | ||||
|         }); | ||||
|       } else { | ||||
|         throw e; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (stream) { | ||||
|       videoElem.srcObject = stream; | ||||
|       // videoElem.src = window.URL.createObjectURL(stream);
 | ||||
|       this.videoElem = videoElem; | ||||
|       this.stream = stream; | ||||
| 
 | ||||
|       const code = await this.decodeFromVideo(videoElem, opts); | ||||
|       return code; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Prepares the video element for video file. | ||||
|    * | ||||
|    * @param  {DOMElement} videoElem <video> dom element | ||||
|    * @param  {Object} options     options (optional) - Additional options. | ||||
|    *  inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst) | ||||
|    *  refer to jsqr options: https://github.com/cozmo/jsQR
 | ||||
|    */ | ||||
|   async decodeFromVideo(videoElem: HTMLVideoElement, options = {}) { | ||||
|     const opts = { | ||||
|       ...this.defaultOption, | ||||
|       ...options, | ||||
|     }; | ||||
|     try { | ||||
|       this.videoElem = videoElem; | ||||
|       const code = await this._captureToCanvas(videoElem, opts); | ||||
|       return code; | ||||
|     } catch (e) { | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Decodes an image from its src. | ||||
|    * @param  {DOMElement} imageElem | ||||
|    * @param  {Object} options     options (optional) - Additional options. | ||||
|    *  inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst) | ||||
|    *  refer to jsqr options: https://github.com/cozmo/jsQR
 | ||||
|    */ | ||||
|   async decodeFromImage( | ||||
|     img: HTMLImageElement | string, | ||||
|     options: { crossOrigin?: string } = {}, | ||||
|   ) { | ||||
|     let imgDom: HTMLImageElement | null = null; | ||||
|     const opts = { | ||||
|       ...this.defaultOption, | ||||
|       ...options, | ||||
|     }; | ||||
| 
 | ||||
|     if (typeof img === 'string') { | ||||
|       imgDom = document.createElement('img'); | ||||
|       if (options.crossOrigin) { | ||||
|         imgDom.crossOrigin = options.crossOrigin; | ||||
|       } | ||||
|       imgDom.src = img; | ||||
|       const proms = () => | ||||
|         new Promise((resolve) => { | ||||
|           imgDom!.onload = () => resolve(true); | ||||
|         }); | ||||
|       await proms(); | ||||
|     } else if (+img.nodeType > 0) { | ||||
|       if (!img.src) { | ||||
|         throw new Error('The ImageElement must contain a src'); | ||||
|       } | ||||
|       imgDom = img; | ||||
|     } | ||||
| 
 | ||||
|     let code = null; | ||||
|     if (imgDom) { | ||||
|       code = this._decodeFromImageElm(imgDom, opts); | ||||
|     } | ||||
|     return code; | ||||
|   } | ||||
| 
 | ||||
|   _decodeFromImageElm(imgObj: HTMLImageElement, options = {}) { | ||||
|     const opts: Options = { | ||||
|       ...this.defaultOption, | ||||
|       ...options, | ||||
|     }; | ||||
|     const imageData = this._createImageData( | ||||
|       imgObj, | ||||
|       imgObj.width, | ||||
|       imgObj.height, | ||||
|     ); | ||||
| 
 | ||||
|     const code = jsQR(imageData.data, imageData.width, imageData.height, opts); | ||||
| 
 | ||||
|     if (code) { | ||||
|       return code; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Releases a video stream that was being | ||||
|    * captured by prepareToVideo | ||||
|    */ | ||||
|   stop() { | ||||
|     if (this.stream) { | ||||
|       const track = this.stream.getTracks()[0]; | ||||
|       track.stop(); | ||||
|       this.stream = null; | ||||
| 
 | ||||
|       // fix: clear black bg after camera capture
 | ||||
|       this.videoElem!.srcObject = null; | ||||
|     } | ||||
| 
 | ||||
|     if (this.timerCapture) { | ||||
|       clearTimeout(this.timerCapture); | ||||
|       this.timerCapture = null; | ||||
|     } | ||||
| 
 | ||||
|     return this; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Sets the sourceId for the camera to use. | ||||
|    */ | ||||
|   setGroupId(groupId: string) { | ||||
|     if (groupId) { | ||||
|       this.videoConstraints.video = { | ||||
|         advanced: [{ groupId }], | ||||
|       }; | ||||
|     } else { | ||||
|       this.videoConstraints.video = true; | ||||
|     } | ||||
| 
 | ||||
|     return this; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Gets a list of all available video sources on | ||||
|    * the current device. | ||||
|    */ | ||||
|   async getVideoDevices() { | ||||
|     if (navigator.mediaDevices.enumerateDevices) { | ||||
|       const devices = await navigator.mediaDevices.enumerateDevices(); | ||||
|       return devices.filter((item: MediaDeviceInfo) => { | ||||
|         if (item.kind === 'videoinput') { | ||||
|           return true; | ||||
|         } | ||||
|         return false; | ||||
|       }); | ||||
|     } else { | ||||
|       throw new Error( | ||||
|         'Current browser doest not support MediaStreamTrack.getSources', | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default QrcodeDecoder; | ||||
							
								
								
									
										6
									
								
								next.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								next.config.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| /** @type {import('next').NextConfig} */ | ||||
| const nextConfig = { | ||||
|   reactStrictMode: true, | ||||
| } | ||||
| 
 | ||||
| module.exports = nextConfig | ||||
							
								
								
									
										3569
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3569
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										26
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| { | ||||
|   "name": "qr-decoder", | ||||
|   "version": "0.1.0", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "dev": "next dev", | ||||
|     "build": "next build && next export", | ||||
|     "start": "next start", | ||||
|     "lint": "next lint" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@next/font": "13.1.6", | ||||
|     "@types/node": "18.13.0", | ||||
|     "@types/react": "18.0.28", | ||||
|     "@types/react-dom": "18.0.10", | ||||
|     "jsqr": "^1.4.0", | ||||
|     "next": "13.1.6", | ||||
|     "react": "18.2.0", | ||||
|     "react-dom": "18.2.0", | ||||
|     "typescript": "4.9.5" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "eslint": "8.34.0", | ||||
|     "eslint-config-next": "13.1.6" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										6
									
								
								src/pages/_app.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/pages/_app.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| import '@/styles/globals.css' | ||||
| import type { AppProps } from 'next/app' | ||||
| 
 | ||||
| export default function App({ Component, pageProps }: AppProps) { | ||||
|   return <Component {...pageProps} /> | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/pages/_document.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/pages/_document.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| import { Html, Head, Main, NextScript } from 'next/document' | ||||
| 
 | ||||
| export default function Document() { | ||||
|   return ( | ||||
|     <Html lang="en"> | ||||
|       <Head /> | ||||
|       <body> | ||||
|         <Main /> | ||||
|         <NextScript /> | ||||
|       </body> | ||||
|     </Html> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										67
									
								
								src/pages/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/pages/index.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,67 @@ | |||
| import Head from 'next/head' | ||||
| 
 | ||||
| import { useGlueJar } from "../../lib/gluejar"; | ||||
| import QrcodeDecoder from '../../lib/qr'; | ||||
| import { useState } from 'react'; | ||||
| import { QRCode } from 'jsqr'; | ||||
| 
 | ||||
| function QRImage({ image }: { image: string }) { | ||||
|   const [data, setData] = useState<QRCode>(); | ||||
|   const [errored, setErrored] = useState(false); | ||||
| 
 | ||||
|   const qr = new QrcodeDecoder(); | ||||
|   qr.decodeFromImage(image).then(data => { | ||||
|     if (data != null && typeof data === "object") { | ||||
|       setData(data) | ||||
|     } else { | ||||
|       setErrored(true); | ||||
|     } | ||||
|   }).catch(err => { | ||||
|     setData(undefined); | ||||
|     setErrored(true); | ||||
|   }); | ||||
| 
 | ||||
|   return <> | ||||
|     <article> | ||||
|       <img src={image} alt={"Pasted image: " + image} /> | ||||
|       {errored | ||||
|         ? <strong>There was an error decoding this QR code.</strong> | ||||
|         : data === undefined | ||||
|           ? <p>Parsing...</p> | ||||
|           : <code>{data.data}</code>} | ||||
|     </article> | ||||
|   </> | ||||
| } | ||||
| 
 | ||||
| export default function Home() { | ||||
| 	const { pasted, error } = useGlueJar(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="app"> | ||||
|         <Head> | ||||
|           <title>QR Decoder</title> | ||||
|           <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
|         </Head> | ||||
|         <main> | ||||
|           <header> | ||||
|           <h1>QR Decoder</h1> | ||||
|           <p>{"Paste an image :)"}</p> | ||||
|           </header> | ||||
| 
 | ||||
|           {error !== null && <span>{error}</span>} | ||||
|           <div> | ||||
|             {pasted.length > 0 && | ||||
|               pasted.map((image) => ( | ||||
|                 <QRImage image={image} key={image} /> | ||||
|               ))} | ||||
|           </div> | ||||
|         </main> | ||||
| 
 | ||||
|         <footer> | ||||
|             A web tool by <a href="https://som.codes">Charlotte Som</a>. | ||||
|         </footer> | ||||
|       </div> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										53
									
								
								src/styles/globals.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/styles/globals.css
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| :root { | ||||
|   font-family: sans-serif; | ||||
| } | ||||
| 
 | ||||
| main { | ||||
| } | ||||
| 
 | ||||
| img { | ||||
|   display: block; | ||||
|   max-width: 100%; | ||||
|   margin: 0 auto; | ||||
| } | ||||
| 
 | ||||
| article { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   border: 1px solid black; | ||||
|   border-radius: 6px; | ||||
|   padding: 2em; | ||||
|   margin-top: 2em; | ||||
| } | ||||
| 
 | ||||
| code { | ||||
|   font-size: 1.125rem; | ||||
|   font-family: monospace; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| .app { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   min-height: 100vh; | ||||
| } | ||||
| 
 | ||||
| main { | ||||
|   flex: 1; | ||||
|   width: 100%; | ||||
|   max-width: 960px; | ||||
|   margin: 0 auto; | ||||
| } | ||||
| 
 | ||||
| footer { | ||||
|   text-align: center; | ||||
|   font-size: 0.85em; | ||||
|   padding: 2em; | ||||
| } | ||||
| 
 | ||||
| p { | ||||
|   margin: 0; | ||||
| } | ||||
							
								
								
									
										24
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "es5", | ||||
|     "lib": ["dom", "dom.iterable", "esnext"], | ||||
|     "allowJs": true, | ||||
|     "skipLibCheck": true, | ||||
|     "strict": true, | ||||
|     "forceConsistentCasingInFileNames": true, | ||||
|     "noEmit": true, | ||||
|     "esModuleInterop": true, | ||||
|     "module": "esnext", | ||||
|     "moduleResolution": "node", | ||||
|     "resolveJsonModule": true, | ||||
|     "isolatedModules": true, | ||||
|     "jsx": "preserve", | ||||
|     "incremental": true, | ||||
|     "baseUrl": ".", | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     } | ||||
|   }, | ||||
|   "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], | ||||
|   "exclude": ["node_modules"] | ||||
| } | ||||
		Loading…
	
		Reference in a new issue