diff --git a/package.json b/package.json index d3742dc..238be86 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@prisma/client": "^6.6.0", "@trafficlunar/asmcrypto.js": "^1.0.2", "bit-buffer": "^0.2.5", + "canvas-confetti": "^1.9.3", "dayjs": "^1.11.13", "downshift": "^9.0.9", "embla-carousel-react": "^8.6.0", @@ -38,6 +39,7 @@ "@eslint/eslintrc": "^3.3.1", "@iconify/react": "^5.2.1", "@tailwindcss/postcss": "^4.1.3", + "@types/canvas-confetti": "^1.9.0", "@types/node": "^20.17.30", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 093f2b9..3a0fe89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: bit-buffer: specifier: ^0.2.5 version: 0.2.5 + canvas-confetti: + specifier: ^1.9.3 + version: 1.9.3 dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -84,6 +87,9 @@ importers: '@tailwindcss/postcss': specifier: ^4.1.3 version: 4.1.3 + '@types/canvas-confetti': + specifier: ^1.9.0 + version: 1.9.0 '@types/node': specifier: ^20.17.30 version: 20.17.30 @@ -815,6 +821,9 @@ packages: '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/canvas-confetti@1.9.0': + resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -1081,6 +1090,9 @@ packages: caniuse-lite@1.0.30001712: resolution: {integrity: sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==} + canvas-confetti@1.9.3: + resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2850,6 +2862,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/canvas-confetti@1.9.0': {} + '@types/cookie@0.6.0': {} '@types/estree@1.0.7': {} @@ -3138,6 +3152,8 @@ snapshots: caniuse-lite@1.0.30001712: {} + canvas-confetti@1.9.3: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 4f18af6..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,6 +0,0 @@ -onlyBuiltDependencies: - - '@prisma/client' - - '@prisma/engines' - - esbuild - - prisma - - sharp diff --git a/public/tutorial/allow-copying/step2.png b/public/tutorial/allow-copying/step2.png new file mode 100644 index 0000000..9c2b295 Binary files /dev/null and b/public/tutorial/allow-copying/step2.png differ diff --git a/public/tutorial/allow-copying/step3.png b/public/tutorial/allow-copying/step3.png new file mode 100644 index 0000000..307b5f6 Binary files /dev/null and b/public/tutorial/allow-copying/step3.png differ diff --git a/public/tutorial/allow-copying/step4.png b/public/tutorial/allow-copying/step4.png new file mode 100644 index 0000000..f91cc5f Binary files /dev/null and b/public/tutorial/allow-copying/step4.png differ diff --git a/public/tutorial/allow-copying/step5.png b/public/tutorial/allow-copying/step5.png new file mode 100644 index 0000000..93b0cf5 Binary files /dev/null and b/public/tutorial/allow-copying/step5.png differ diff --git a/public/tutorial/allow-copying/step6.png b/public/tutorial/allow-copying/step6.png new file mode 100644 index 0000000..67bae80 Binary files /dev/null and b/public/tutorial/allow-copying/step6.png differ diff --git a/public/tutorial/allow-copying/step7.png b/public/tutorial/allow-copying/step7.png new file mode 100644 index 0000000..b30964d Binary files /dev/null and b/public/tutorial/allow-copying/step7.png differ diff --git a/public/tutorial/allow-copying/thumbnail.png b/public/tutorial/allow-copying/thumbnail.png new file mode 100644 index 0000000..ab54c2a Binary files /dev/null and b/public/tutorial/allow-copying/thumbnail.png differ diff --git a/public/tutorial/create-qr-code/step2.png b/public/tutorial/create-qr-code/step2.png new file mode 100644 index 0000000..60a1eb9 Binary files /dev/null and b/public/tutorial/create-qr-code/step2.png differ diff --git a/public/tutorial/create-qr-code/step3.png b/public/tutorial/create-qr-code/step3.png new file mode 100644 index 0000000..dbc6743 Binary files /dev/null and b/public/tutorial/create-qr-code/step3.png differ diff --git a/public/tutorial/create-qr-code/step4.png b/public/tutorial/create-qr-code/step4.png new file mode 100644 index 0000000..562b19f Binary files /dev/null and b/public/tutorial/create-qr-code/step4.png differ diff --git a/public/tutorial/create-qr-code/step5.png b/public/tutorial/create-qr-code/step5.png new file mode 100644 index 0000000..e6cf9fd Binary files /dev/null and b/public/tutorial/create-qr-code/step5.png differ diff --git a/public/tutorial/create-qr-code/step6.png b/public/tutorial/create-qr-code/step6.png new file mode 100644 index 0000000..f78da9a Binary files /dev/null and b/public/tutorial/create-qr-code/step6.png differ diff --git a/public/tutorial/create-qr-code/thumbnail.png b/public/tutorial/create-qr-code/thumbnail.png new file mode 100644 index 0000000..cfb7906 Binary files /dev/null and b/public/tutorial/create-qr-code/thumbnail.png differ diff --git a/public/tutorial/step1.png b/public/tutorial/step1.png new file mode 100644 index 0000000..13983f9 Binary files /dev/null and b/public/tutorial/step1.png differ diff --git a/src/components/submit-form/index.tsx b/src/components/submit-form/index.tsx index f72d68a..e5fa7ed 100644 --- a/src/components/submit-form/index.tsx +++ b/src/components/submit-form/index.tsx @@ -17,6 +17,7 @@ import TagSelector from "../tag-selector"; import ImageList from "./image-list"; import QrUpload from "./qr-upload"; import QrScanner from "./qr-scanner"; +import SubmitTutorialButton from "../tutorial/submit"; import LikeButton from "../like-button"; import Carousel from "../carousel"; import SubmitButton from "../submit-button"; @@ -131,13 +132,7 @@ export default function SubmitForm() {
- URL.createObjectURL(file)), - ]} - /> + URL.createObjectURL(file))]} />

@@ -204,7 +199,6 @@ export default function SubmitForm() {
- or +
{/* Separator */} -
+

Custom images
diff --git a/src/components/tutorial/page.tsx b/src/components/tutorial/page.tsx new file mode 100644 index 0000000..4a04e11 --- /dev/null +++ b/src/components/tutorial/page.tsx @@ -0,0 +1,59 @@ +"use client"; + +import Image from "next/image"; +import { Icon } from "@iconify/react"; +import { useEffect } from "react"; + +import confetti from "canvas-confetti"; + +interface Props { + text?: string; + imageSrc?: string; + carouselIndex?: number; + finishIndex?: number; +} + +export default function TutorialPage({ text, imageSrc, carouselIndex, finishIndex }: Props) { + useEffect(() => { + if (carouselIndex !== finishIndex || !carouselIndex || !finishIndex) return; + + const defaults = { startVelocity: 30, spread: 360, ticks: 120, zIndex: 50 }; + const randomInRange = (min: number, max: number) => Math.random() * (max - min) + min; + + setTimeout(() => { + confetti({ + ...defaults, + particleCount: 500, + origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }, + }); + confetti({ + ...defaults, + particleCount: 500, + origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }, + }); + }, 300); + }, [carouselIndex, finishIndex]); + + return ( +
+ {!finishIndex ? ( + <> +

{text}

+ + step 1 image + + ) : ( +
+ +

Yatta! You did it!

+
+ )} +
+ ); +} diff --git a/src/components/tutorial/starting-page.tsx b/src/components/tutorial/starting-page.tsx new file mode 100644 index 0000000..e4ef9cd --- /dev/null +++ b/src/components/tutorial/starting-page.tsx @@ -0,0 +1,59 @@ +import Image from "next/image"; +import { UseEmblaCarouselType } from "embla-carousel-react"; + +interface Props { + emblaApi: UseEmblaCarouselType[1] | undefined; +} + +export default function StartingPage({ emblaApi }: Props) { + const goToTutorial = (index: number) => { + if (!emblaApi) return; + + emblaApi.scrollTo(index - 1, true); + emblaApi.scrollTo(index); + }; + + return ( +
+ {/* Separator */} +
+
+ Pick a tutorial +
+
+ +
+ + + +
+
+ ); +} diff --git a/src/components/tutorial/submit.tsx b/src/components/tutorial/submit.tsx new file mode 100644 index 0000000..50700ca --- /dev/null +++ b/src/components/tutorial/submit.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import useEmblaCarousel from "embla-carousel-react"; +import { Icon } from "@iconify/react"; + +import TutorialPage from "./page"; +import StartingPage from "./starting-page"; + +export default function SubmitTutorialButton() { + const [isOpen, setIsOpen] = useState(false); + const [isVisible, setIsVisible] = useState(false); + + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }); + const [selectedIndex, setSelectedIndex] = useState(0); + + const close = () => { + setIsVisible(false); + setTimeout(() => { + setIsOpen(false); + setSelectedIndex(0); + }, 300); + }; + + useEffect(() => { + if (isOpen) { + // slight delay to trigger animation + setTimeout(() => setIsVisible(true), 10); + } + }, [isOpen]); + + useEffect(() => { + if (!emblaApi) return; + emblaApi.on("select", () => setSelectedIndex(emblaApi.selectedScrollSnap())); + }, [emblaApi]); + + const isStartingPage = selectedIndex === 0 || selectedIndex === 9; + const inTutorialAllowCopying = selectedIndex && selectedIndex >= 1 && selectedIndex <= 9; + + return ( + <> + + + {isOpen && + createPortal( +
+
+ +
+
+

Tutorial

+ +
+ +
+
+
+ + + {/* Allow Copying */} + + + + + + + + + + + + {/* Create QR Code */} + + + + + + + +
+
+ +
+ + + {inTutorialAllowCopying ? "Allow Copying" : "Create QR Code"} + + +
+
+
+
, + document.body + )} + + ); +}