Skip to content

Commit

Permalink
[Feature] Add a basic QR Code generator (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
inssein authored Nov 11, 2023
1 parent d28a490 commit 14f3f92
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 7 deletions.
7 changes: 6 additions & 1 deletion app/components/box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@ export function BoxButtons({ children }: PropsWithChildren<{}>) {
export function BoxOptions({
isLast,
children,
}: PropsWithChildren<{ readonly isLast: Boolean }>) {
className,
}: PropsWithChildren<{
readonly isLast: Boolean;
readonly className?: string | undefined;
}>) {
return (
<div
className={classNames(
"px-3 py-2 flex border-t border-gray-600 bg-zinc-800/50 items-center",
isLast && "rounded-b-lg",
className,
)}
>
{children}
Expand Down
9 changes: 7 additions & 2 deletions app/components/icon-button.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import * as React from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
import type { Placement } from "@floating-ui/core/src/types";
import type { Placement } from "@floating-ui/react";

interface Props {
readonly icon: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>;
readonly icon: React.ForwardRefExoticComponent<
React.PropsWithoutRef<React.SVGProps<SVGSVGElement>> & {
title?: string;
titleId?: string;
} & React.RefAttributes<SVGSVGElement>
>;
readonly label: string;
readonly onClick: () => void;
readonly tooltipPlacement?: Placement;
Expand Down
1 change: 1 addition & 0 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export default class Routes {
static readonly WHOIS = "/whois";
static readonly UNIX_TIMESTAMP = "/unix-timestamp";
static readonly SQL_FORMATTER = "/sql-formatter";
static readonly QR_CODE = "/qr-code";
}
2 changes: 1 addition & 1 deletion app/routes/cuid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function UuidGenerator() {
<div className="flex items-center justify-start flex-grow">
<label
htmlFor="length"
className="block text-sm font-medium text-gray-900 dark:text-white"
className="block text-sm font-medium text-gray-900 dark:text-white pr-2"
>
Length:
</label>
Expand Down
2 changes: 1 addition & 1 deletion app/routes/dataurl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default function DataUrl() {
<BoxTitle title="Output"></BoxTitle>
<BoxContent
isLast={true}
className="max-h-full flex justify-center"
className="max-h-full flex justify-center py-4"
>
<img className="max-w-full" alt="Output" src={output} />
</BoxContent>
Expand Down
175 changes: 175 additions & 0 deletions app/routes/qr-code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { useCallback, useMemo, useRef, useState } from "react";
import Utiliti from "~/components/utiliti";
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
import Box, { BoxContent, BoxOptions, BoxTitle } from "~/components/box";
import { DocumentArrowDownIcon } from "@heroicons/react/24/outline";
import IconButton from "~/components/icon-button";

enum Action {
GENERATE = "Generate",
}

export default function QrCode() {
const [svg, setSvg] = useState(false);
const [background, setBackground] = useState("#ffffff");
const [foreground, setForeground] = useState("#000000");
const componentRef = useRef<HTMLDivElement>(null);

const actions = useMemo(
() => ({
[Action.GENERATE]: async (input: string) => input,
}),
[],
);

const download = useCallback(() => {
const node = componentRef.current?.children[0];

if (!node) {
return;
}

const uri = svg
? svgElementToUri(node as SVGElement)
: (node as HTMLCanvasElement).toDataURL();
const extension = svg ? "svg" : "png";

const link = document.createElement("a");
link.download = "qr-code." + extension;
link.href = uri;
link.click();
}, [svg]);

const renderOutput = useCallback(
(_: string, input: string) => {
const Component = svg ? QRCodeSVG : QRCodeCanvas;

return (
<Box>
<BoxTitle title="Output">
<IconButton
icon={DocumentArrowDownIcon}
label="Download"
onClick={download}
/>
</BoxTitle>
<BoxContent
isLast={true}
className="max-h-full flex justify-center py-4"
>
<div ref={componentRef}>
<Component
value={input}
size={256}
bgColor={background}
fgColor={foreground}
/>
</div>
</BoxContent>
</Box>
);
},
[background, download, foreground, svg],
);

return (
<Utiliti
label="QR Code"
actions={actions}
renderInput={(input, setInput) => (
<textarea
id="input"
rows={3}
className="block font-mono w-full px-3 py-2 lg:text-sm border-0 bg-zinc-800 focus:ring-0 text-white placeholder-zinc-400"
placeholder="Paste in your URL…"
required={true}
value={input}
onChange={(e) => setInput(e.target.value)}
></textarea>
)}
renderOptions={() => (
<BoxOptions isLast={false}>
<div>
<div className="flex flex-row pb-1">
<div className="flex items-center h-5 w-5">
<input
id="svg"
type="checkbox"
checked={svg}
className="w-4 h-4 border rounded focus:ring-3 bg-zinc-700 border-zinc-600 text-orange-600 focus:ring-orange-600 ring-offset-zinc-800 focus:ring-offset-zinc-800"
onChange={(e) => setSvg(e.target.checked)}
/>
</div>
<label
htmlFor="svg"
className="ml-2 text-sm font-medium text-gray-300"
>
SVG
</label>
</div>

<div className="flex flex-row pb-1">
<div className="flex items-center h-5 w-5">
<input
type="color"
className="block bg-zinc-700 rounded"
style={{
width: "1.05rem",
height: "1.05rem",
}}
id="background"
defaultValue={background}
onChange={(e) => setBackground(e.target.value)}
title="Choose your color"
/>
</div>
<label
htmlFor="background"
className="ml-2 text-sm font-medium text-gray-300"
>
Background
</label>
</div>

<div className="flex flex-row pb-1">
<div className="flex items-center h-5 w-5">
<input
type="color"
className="block bg-zinc-700 rounded"
style={{
width: "1.05rem",
height: "1.05rem",
}}
id="foreground"
defaultValue={foreground}
onChange={(e) => setForeground(e.target.value)}
title="Choose your color"
/>
</div>
<label
htmlFor="foreground"
className="ml-2 text-sm font-medium text-gray-300"
>
Foreground
</label>
</div>
</div>
</BoxOptions>
)}
renderOutput={renderOutput}
showLoadFile={false}
/>
);
}

function svgElementToUri(element: SVGElement) {
const serializer = new XMLSerializer();

return (
"data:image/svg+xml;charset=utf-8," +
encodeURIComponent(
'<?xml version="1.0" standalone="no"?>\r\n' +
serializer.serializeToString(element),
)
);
}
3 changes: 1 addition & 2 deletions app/routes/sql-formatter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import ContentWrapper from "~/components/content-wrapper";
import { utilities } from "~/utilities";
import { metaHelper } from "~/utils/meta";
import Code from "~/components/code";
import type { FormatOptionsWithLanguage, KeywordCase } from "sql-formatter";
import { format } from "sql-formatter";
import { Transition } from "@headlessui/react";
import Copy from "~/components/copy";
import { noop } from "~/common";
import { useLocalStorage } from "~/hooks/use-local-storage";
import NumberInput from "~/components/number-input";
import type { KeywordCase } from "sql-formatter/lib/src/FormatOptions";
import type { FormatOptionsWithLanguage } from "sql-formatter/lib/src/sqlFormatter";
import { useHydrated } from "~/hooks/use-hydrated";

export const meta = metaHelper(
Expand Down
12 changes: 12 additions & 0 deletions app/styles/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
margin-right: calc(50% - min(50%, 33rem));
}

input[type="color"] {
-webkit-appearance: none;
border: none;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 0.25rem;
}

@tailwind base;
@tailwind components;
@tailwind utilities;
6 changes: 6 additions & 0 deletions app/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ export const utilities: Record<
description: "Format your SQL queries before sharing them with others.",
url: Routes.SQL_FORMATTER,
},
qrCode: {
name: "QR Code",
description: "Easily generate a QR code and download it.",
url: Routes.QR_CODE,
},
};

export const sidebar = [
Expand All @@ -90,6 +95,7 @@ export const sidebar = [
utilities.url,
utilities.base64,
utilities.sqlFormatter,
utilities.qrCode,
],
},
{
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"highlight.js": "^11.9.0",
"isbot": "^3.7.0",
"prettier": "^3.0.3",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-json-tree": "^0.18.0",
Expand Down

0 comments on commit 14f3f92

Please sign in to comment.