diff --git a/src/config.ts b/src/config.ts index dafdfa3..5019fa4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,6 +4,12 @@ export type BodyShape = "square" | "circle"; export interface Config { length: number; + value: string; + logo?: { + url: string; + height: number; + width: number; + }; shapes: { eyeFrame: EyeFrameShape; body: BodyShape; @@ -26,24 +32,36 @@ export interface Config { } export const defaultConfig: Config = { - length: 400, + length: 200, + value: "https://intosoft.com", + logo: { + url: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjIuMSAxOTQuNyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiM0MmM5ODU7fS5jbHMtMntmaWxsOiM2NmJmODM7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNOTQuMzYsNzUuNTljMTguMzksMTkuNzUsMTguNzYsNDkuMjguODIsNjZsLTUuMjQsNC44Ny03LjQ5LDctMy4xNSwzLjEyTDQwLjQ3LDE5Mi44N2E2Ljc3LDYuNzcsMCwwLDEtOS41Ny0uMzFMMCwxNTkuNTVsMTguNi0xNy40MWEyMi43OCwyMi43OCwwLDAsMSwxLjU5LTEuNjhsLjE1LS4xNCw4LjE3LTcuNjIsOS4wOS04LjQ3LjkyLS44NywxMS4yNy0xMC41MWExOSwxOSwwLDAsMCwxMi40NSwzLjYzLDE5LjE2LDE5LjE2LDAsMSwwLTE3LjQ0LTlMMzEuMTQsMTIwLjI1QTEuODEsMS44MSwwLDAsMSwyOC42LDEyMGwtLjg2LS45MkM5LjM1LDk5LjM3LDksNjkuODQsMjYuOTIsNTMuMTZMMzkuNjUsNDEuMzFsMzgtMzUuNzZMNzIsMTAuODhsOS42NC05LjA2YTYuNzMsNi43MywwLDAsMSw5LjUyLjMxbDMwLjkyLDMzTDEwMy41LDUyLjU3YTIwLjg1LDIwLjg1LDAsMCwxLTEuNTksMS42OGwtLjE1LjE0TDkzLjYsNjIsODcuMDcsNjguMWw2LjE1LDYuMzJaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNOTQuMzYsNzUuNTlsLTEuMTQtMS4xN2EyLjI2LDIuMjYsMCwwLDEsLjI5LjI1WiIvPjwvZz48L2c+PC9zdmc+", + height: 70, + width: 105, + }, shapes: { eyeFrame: "circle", - body: "square", + body: "circle", eyeball: "circle", }, colors: { background: "white", - body: "black", + body: "linear-gradient(90deg, rgba(255,31,234,1) 4%, RGBA(225,147,129,1) 35%, rgba(0,212,255,1) 100%)", eyeFrame: { - topLeft: "black", - topRight: "black", - bottomLeft: "black", + topLeft: + "linear-gradient(90deg, RGBA(66, 58, 187, 1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)", + topRight: + "linear-gradient(90deg, RGBA(66, 58, 187, 1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)", + bottomLeft: + "linear-gradient(90deg, RGBA(66, 58, 187, 1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)", }, eyeball: { - topLeft: "black", - topRight: "black", - bottomLeft: "black", + topLeft: + "linear-gradient(90deg, rgba(244,209,74,1) 0%, RGB(5, 5, 5) 35%, rgba(0,212,255,1) 100%)", + topRight: + "linear-gradient(90deg, rgba(244,209,74,1) 0%, RGB(5, 5, 5) 35%, rgba(0,212,255,1) 100%)", + bottomLeft: + "linear-gradient(90deg, rgba(244,209,74,1) 0%, RGB(5, 5, 5) 35%, rgba(0,212,255,1) 100%)", }, }, }; diff --git a/src/index.ts b/src/index.ts index 1a48499..6c7bfc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ import { squareEyeFrame, } from "./eyeframes"; import { generatePath } from "./path"; -import { generateMatrix } from "./utils"; +import { generateMatrix, renderLogoFromConfig } from "./utils"; const quietZone = 0; @@ -35,7 +35,7 @@ const eyeballFunction: { }; export const generateSVGString = (config: Config) => { - const matrix = generateMatrix("https://intosoft.com", "L"); + const matrix = generateMatrix(config.value || "https://intosoft.com", "L"); const path = generatePath({ matrix, size: config.length, config }); @@ -48,16 +48,16 @@ export const generateSVGString = (config: Config) => { config.length }" xmlns="http://www.w3.org/2000/svg"> - ${generateLinearGradientByConfig(config)} + ${generateLinearGradientByConfig(config)} - - - - + + ${renderLogoFromConfig(config)} ${generateEyeFrameSVGFromConfig(config, matrix.length)} ${generateEyeballSVGFromConfig(config, matrix.length)} - + `; return svg; diff --git a/src/utils.ts b/src/utils.ts index 6db731d..cf45b26 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,4 @@ +import { Config } from "./config"; import QRCode, { QRCodeErrorCorrectionLevel } from "qrcode"; export const generateMatrix = ( @@ -135,3 +136,14 @@ export const getPositionForEyes = ({ }, }; }; + +export const renderLogoFromConfig = (config: Config) => { + if (config.logo?.url) { + const centerX = (config.length - config.logo.width) / 2; + const centerY = (config.length - config.logo.height) / 2; + return ``; + } + + return ""; +}; diff --git a/web/package.json b/web/package.json index 986ff43..58d383b 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ "@types/react": "^18.2.58", "@types/react-dom": "^18.2.19", "copy-to-clipboard": "^3.3.3", + "lodash": "^4.17.21", "rc-slider": "^10.5.0", "react": "^18.2.0", "react-best-gradient-color-picker": "^3.0.7", @@ -49,6 +50,7 @@ }, "devDependencies": { "@craco/craco": "^7.1.0", + "@types/lodash": "^4.14.202", "@types/react-color": "^3.0.12", "@types/react-syntax-highlighter": "^15.5.11", "customize-cra": "^1.0.0", diff --git a/web/src/pages/Home/Code.tsx b/web/src/pages/Home/Code.tsx index ae89b88..cc14265 100644 --- a/web/src/pages/Home/Code.tsx +++ b/web/src/pages/Home/Code.tsx @@ -21,28 +21,29 @@ type Platform = | "vanilla"; const CODES: { [key in Platform]: string } = { - react: ` -import QRCode from "@intosoft/qrcode/react"; + react: `import { generateSVGString } from '@intosoft/qrcode'; -export const QRCodeRenderer = () => { - return -} + const svgString = generateSVGString(config); + export const RenderQR = () => { + return (
); + }; `, - "react-native": ` -// Prerequisite + "react-native": `// Prerequisite // npm i react-native-svg | yarn add react-native-svg -import QRCode from '@intosoft/qrcode/native'; +import { SvgFromXml } from "react-native-svg"; +import { generateSVGString } from '@intosoft/qrcode'; -export const QRCodeRenderer = () => { - return -} +const svgString = generateSVGString(config); + +export const RenderQR = () => { + return (); +}; `, node: ``, angular: ``, vue: ``, - vanilla: ` - + vanilla: `
@@ -83,6 +84,8 @@ const PlatformCode = ({ id }: { id: Platform }) => { style={materialDark} customStyle={{ width: "100%", + fontSize: 14, + borderRadius: 4, }} > {CODES[id]} @@ -146,6 +149,8 @@ export const CodeBlock = ({ config }: CodeBlockProps) => { style={materialDark} customStyle={{ width: "100%", + fontSize: 14, + borderRadius: 4, }} > {codeString} diff --git a/web/src/pages/Home/customization/Colors.tsx b/web/src/pages/Home/customization/Colors.tsx index 9e227d6..4205bbb 100644 --- a/web/src/pages/Home/customization/Colors.tsx +++ b/web/src/pages/Home/customization/Colors.tsx @@ -72,60 +72,60 @@ export const Colors = ({ label: "Background", value: qrConfig.colors.background, onChange: (value: string) => { - setQrConfig((prev) => ({ - ...prev, + setQrConfig({ + ...qrConfig, colors: { - ...prev.colors, + ...qrConfig.colors, background: value, }, - })); + }); }, }, { label: "Body", value: qrConfig.colors.body, onChange: (value: string) => { - setQrConfig((prev) => ({ - ...prev, + setQrConfig({ + ...qrConfig, colors: { - ...prev.colors, + ...qrConfig.colors, body: value, }, - })); + }); }, }, { label: "EyeFrame", value: qrConfig.colors.eyeFrame.topLeft, onChange: (value: string) => { - setQrConfig((prev) => ({ - ...prev, + setQrConfig({ + ...qrConfig, colors: { - ...prev.colors, + ...qrConfig.colors, eyeFrame: { topLeft: value, topRight: value, bottomLeft: value, }, }, - })); + }); }, }, { label: "Eyeball", value: qrConfig.colors.eyeball.topLeft, onChange: (value: string) => { - setQrConfig((prev) => ({ - ...prev, + setQrConfig({ + ...qrConfig, colors: { - ...prev.colors, + ...qrConfig.colors, eyeball: { topLeft: value, topRight: value, bottomLeft: value, }, }, - })); + }); }, }, ]; diff --git a/web/src/pages/Home/customization/Content.tsx b/web/src/pages/Home/customization/Content.tsx new file mode 100644 index 0000000..598304d --- /dev/null +++ b/web/src/pages/Home/customization/Content.tsx @@ -0,0 +1,40 @@ +import styled from "styled-components"; + +import { CustomizationSectionProps } from "./type"; + +const Input = styled.input` + height: 40px; + width: 100%; + border: 1px solid #d0d7df; + border-radius: 4px; + padding: 4px 10px; + outline: none; + + &:focus { + border: 1px solid #a2e344; + } +`; + +const Label = styled.p` + margin: 4px 2px; +`; + +export const Content = ({ + setQrConfig, + qrConfig, +}: CustomizationSectionProps) => { + return ( + <> + + + setQrConfig({ + ...qrConfig, + value, + }) + } + /> + + ); +}; diff --git a/web/src/pages/Home/customization/Logo.tsx b/web/src/pages/Home/customization/Logo.tsx index 9b3a03f..9bc010b 100644 --- a/web/src/pages/Home/customization/Logo.tsx +++ b/web/src/pages/Home/customization/Logo.tsx @@ -1,5 +1,119 @@ +import styled from "styled-components"; + import { CustomizationSectionProps } from "./type"; +import { fileToBase64 } from "../../../utils/file"; + +const Input = styled.input` + height: 40px; + width: 100%; + border: 1px solid #d0d7df; + border-radius: 4px; + padding: 4px 10px; + outline: none; + + &:focus { + border: 1px solid #a2e344; + } +`; + +const Label = styled.p` + margin: 4px 2px; +`; + +const Upload = styled.div` + height: 80px; + width: 100px; + border: 1px solid #d0d7df; + border-radius: 4px; + cursor: pointer; + background-color: white; +`; export const Logo = ({ setQrConfig, qrConfig }: CustomizationSectionProps) => { - return <>; + return ( + <> + + + setQrConfig({ + ...qrConfig, + logo: { + ...(qrConfig.logo || { url: "", height: 0, width: 0 }), + url: value, + }, + }) + } + /> + +

+ OR +

+ + + { + if (files?.[0]) { + const base64 = await fileToBase64(files[0]); + setQrConfig({ + ...qrConfig, + logo: { + ...(qrConfig.logo || { url: "", height: 0, width: 0 }), + url: base64, + }, + }); + } + }} + /> + +

+ Dimensions +

+ + + setQrConfig({ + ...qrConfig, + logo: { + ...(qrConfig.logo || { url: "", height: 0, width: 0 }), + height: parseInt(value), + }, + }) + } + /> + + + setQrConfig({ + ...qrConfig, + logo: { + ...(qrConfig.logo || { url: "", height: 0, width: 0 }), + width: parseInt(value), + }, + }) + } + /> + + ); }; diff --git a/web/src/pages/Home/customization/Shape.tsx b/web/src/pages/Home/customization/Shape.tsx index 4abf656..cb6deb5 100644 --- a/web/src/pages/Home/customization/Shape.tsx +++ b/web/src/pages/Home/customization/Shape.tsx @@ -39,13 +39,13 @@ export const Shape = ({ setQrConfig, qrConfig }: CustomizationSectionProps) => { key={item[0]} $active={item[0] === qrConfig.shapes.body} onClick={() => - setQrConfig((prev) => ({ - ...prev, + setQrConfig({ + ...qrConfig, shapes: { - ...prev.shapes, + ...qrConfig.shapes, body: item[0], }, - })) + }) } > @@ -59,13 +59,13 @@ export const Shape = ({ setQrConfig, qrConfig }: CustomizationSectionProps) => { key={item[0]} $active={item[0] === qrConfig.shapes.eyeball} onClick={() => - setQrConfig((prev) => ({ - ...prev, + setQrConfig({ + ...qrConfig, shapes: { - ...prev.shapes, + ...qrConfig.shapes, eyeball: item[0], }, - })) + }) } > @@ -79,13 +79,13 @@ export const Shape = ({ setQrConfig, qrConfig }: CustomizationSectionProps) => { key={item[0]} $active={item[0] === qrConfig.shapes.eyeFrame} onClick={() => - setQrConfig((prev) => ({ - ...prev, + setQrConfig({ + ...qrConfig, shapes: { - ...prev.shapes, + ...qrConfig.shapes, eyeFrame: item[0], }, - })) + }) } > diff --git a/web/src/pages/Home/customization/type.ts b/web/src/pages/Home/customization/type.ts index 08d4c61..f3c3441 100644 --- a/web/src/pages/Home/customization/type.ts +++ b/web/src/pages/Home/customization/type.ts @@ -1,7 +1,6 @@ -import { Dispatch, SetStateAction } from "react"; import { Config } from "../../../../../src/config"; export interface CustomizationSectionProps { qrConfig: Config; - setQrConfig: Dispatch>; + setQrConfig: (config: Config) => void; } diff --git a/web/src/pages/Home/index.tsx b/web/src/pages/Home/index.tsx index 1798791..aba07d4 100644 --- a/web/src/pages/Home/index.tsx +++ b/web/src/pages/Home/index.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; - +import packageJSON from "../../../package.json"; import { SVG } from "../../components/SVG"; import { useEffect, useState } from "react"; import { generateSVGString } from "../../../../src/index"; @@ -15,6 +15,7 @@ import { Colors } from "./customization/Colors"; import { Logo } from "./customization/Logo"; import { Config, defaultConfig } from "../../../../src/config"; import { CodeBlock } from "./Code"; +import { Content as ContentTab } from "./customization/Content"; const Container = styled.div` background-color: #f7f8fa; @@ -102,6 +103,10 @@ export const HomePage = () => { useState(1); const TABS = [ + { + title: "Content", + Component: ContentTab, + }, { title: "Shape", Component: Shape, @@ -125,87 +130,144 @@ export const HomePage = () => { ); }, [qrConfig]); + const handleSetConfig = (config: Config) => { + localStorage.setItem( + "qr-config", + JSON.stringify({ + config: { + ...config, + length: 200, + }, + version: packageJSON.version, + }) + ); + + setQrConfig(config); + }; + + useEffect(() => { + const _config = localStorage.getItem("qr-config"); + if (_config) { + try { + const { config, version } = JSON.parse(_config); + if (version === packageJSON.version) { + setQrConfig(config); + console.log("set from storage"); + } + } catch (err) { + console.log("Err", err); + } + } + }, []); + return ( - - - setSelectedCustomizationTabIndex(index)} - > - - {TABS.map(({ title }) => ( - {title} - ))} - - + setSelectedCustomizationTabIndex(index)} + style={{ + width: "100%", + }} + > + + {TABS.map(({ title }) => ( + {title} + ))} + + + {TABS.map(({ title, Component }) => ( - {Component({ qrConfig, setQrConfig })} + ))} - - - - + - - - Low - - - {imageSize} x {imageSize} px - + + + + + + Low + + + {imageSize} x {imageSize} px + + + High + + + setImageSize(val as number)} + value={imageSize} + /> - Hight + Download - - setImageSize(val as number)} - value={imageSize} - /> - - Download - - - - downloadSVG({ svgString, downloadType: "png" })} - > - PNG - - downloadSVG({ svgString, downloadType: "jpeg" })} - > - JPEG - - downloadSVG({ svgString, downloadType: "svg" })} - > - SVG - - - - + + + + downloadSVG({ + config: qrConfig, + imageSize, + downloadType: "png", + }) + } + > + PNG + + + downloadSVG({ + config: qrConfig, + imageSize, + downloadType: "jpeg", + }) + } + > + JPEG + + + downloadSVG({ + config: qrConfig, + imageSize, + downloadType: "svg", + }) + } + > + SVG + + + + + diff --git a/web/src/utils/file.ts b/web/src/utils/file.ts index 8a138ad..f003624 100644 --- a/web/src/utils/file.ts +++ b/web/src/utils/file.ts @@ -1,10 +1,30 @@ +import { generateSVGString } from "../../../src"; +import { Config } from "../../../src/config"; +import { cloneDeep } from "lodash"; + interface DownloadSVGParams { - svgString: string; downloadType: "svg" | "png" | "jpeg"; + imageSize: number; + config: Config; } const FILE_NAME = "intosoft-qrcode"; -export const downloadSVG = ({ svgString, downloadType }: DownloadSVGParams) => { +export const downloadSVG = ({ + config, + imageSize, + downloadType, +}: DownloadSVGParams) => { + const sizeConfig = cloneDeep(config); + if (sizeConfig.logo?.url) { + sizeConfig.logo.height = + (sizeConfig.logo.height / sizeConfig.length) * imageSize; + sizeConfig.logo.width = + (sizeConfig.logo.width / sizeConfig.length) * imageSize; + } + const svgString = generateSVGString({ + ...sizeConfig, + length: imageSize, + }); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); @@ -43,3 +63,21 @@ const downloadFile = (url: string, type: string, filename: string) => { document.body.appendChild(link); link.click(); }; + +export const fileToBase64 = (file: File): Promise => { + var reader = new FileReader(); + + reader.readAsDataURL(file); + return new Promise((resolve, reject) => { + reader.onloadend = function () { + if (typeof reader.result === "string") { + resolve(reader.result); + } else { + reject("result is not string"); + } + }; + reader.onerror = () => { + reject("Couldn't process file"); + }; + }); +}; diff --git a/web/yarn.lock b/web/yarn.lock index 857651c..cad7341 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2173,6 +2173,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash@^4.14.202": + version "4.14.202" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" + integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== + "@types/mime@*": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"