feat: tutorial improvements
- refactored - preload images - also changed robots.txt
This commit is contained in:
parent
c9a10ee70b
commit
7a6eb389d9
6 changed files with 278 additions and 313 deletions
|
|
@ -16,7 +16,6 @@ export default function robots(): MetadataRoute.Robots {
|
||||||
"/report/mii/*",
|
"/report/mii/*",
|
||||||
"/report/user/*",
|
"/report/user/*",
|
||||||
"/admin",
|
"/admin",
|
||||||
"/_next/image",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sitemap: `${process.env.NEXT_PUBLIC_BASE_URL}/sitemap.xml`,
|
sitemap: `${process.env.NEXT_PUBLIC_BASE_URL}/sitemap.xml`,
|
||||||
|
|
|
||||||
215
src/components/tutorial/index.tsx
Normal file
215
src/components/tutorial/index.tsx
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useEmblaCarousel from "embla-carousel-react";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import confetti from "canvas-confetti";
|
||||||
|
import ReturnToIsland from "../admin/return-to-island";
|
||||||
|
|
||||||
|
interface Slide {
|
||||||
|
// step is never used, undefined is assumed as a step
|
||||||
|
type?: "start" | "step" | "finish";
|
||||||
|
text?: string;
|
||||||
|
imageSrc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tutorial {
|
||||||
|
title: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
hint?: string;
|
||||||
|
steps: Slide[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tutorials: Tutorial[];
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Tutorial({ tutorials, isOpen, setIsOpen }: Props) {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
// Build index map
|
||||||
|
const slides: Array<Slide & { tutorialTitle: string }> = [];
|
||||||
|
const startSlides: Record<string, number> = {};
|
||||||
|
|
||||||
|
tutorials.forEach((tutorial) => {
|
||||||
|
tutorial.steps.forEach((slide) => {
|
||||||
|
if (slide.type === "start") {
|
||||||
|
startSlides[tutorial.title] = slides.length;
|
||||||
|
}
|
||||||
|
slides.push({ ...slide, tutorialTitle: tutorial.title });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentSlide = slides[selectedIndex];
|
||||||
|
const isStartingPage = currentSlide?.type === "start";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentSlide.type !== "finish") 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);
|
||||||
|
}, [currentSlide]);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectedIndex(0);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToTutorial = (tutorialTitle: string) => {
|
||||||
|
if (!emblaApi) return;
|
||||||
|
const index = startSlides[tutorialTitle];
|
||||||
|
|
||||||
|
// Jump to next starting slide then transition to actual tutorial
|
||||||
|
emblaApi.scrollTo(index, true);
|
||||||
|
emblaApi.scrollTo(index + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
// slight delay to trigger animation
|
||||||
|
setTimeout(() => setIsVisible(true), 10);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!emblaApi) return;
|
||||||
|
emblaApi.on("select", () => setSelectedIndex(emblaApi.selectedScrollSnap()));
|
||||||
|
}, [emblaApi]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40">
|
||||||
|
<div
|
||||||
|
onClick={close}
|
||||||
|
className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${
|
||||||
|
isVisible ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`z-50 bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg w-full max-w-md h-120 transition-discrete duration-300 flex flex-col ${
|
||||||
|
isVisible ? "scale-100 opacity-100" : "scale-75 opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-2 p-6 pb-0">
|
||||||
|
<h2 className="text-xl font-bold">Tutorial</h2>
|
||||||
|
<button onClick={close} aria-label="Close" className="text-red-400 hover:text-red-500 text-2xl cursor-pointer">
|
||||||
|
<Icon icon="material-symbols:close-rounded" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col min-h-0 h-full">
|
||||||
|
<div className="overflow-hidden h-full" ref={emblaRef}>
|
||||||
|
<div className="flex h-full">
|
||||||
|
{slides.map((slide, index) => (
|
||||||
|
<div key={index} className={`shrink-0 flex flex-col w-full px-6 ${slide.type === "start" && "py-6"}`}>
|
||||||
|
{slide.type === "start" ? (
|
||||||
|
<>
|
||||||
|
{/* Separator */}
|
||||||
|
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2">
|
||||||
|
<hr className="grow border-zinc-300" />
|
||||||
|
<span>Pick a tutorial</span>
|
||||||
|
<hr className="grow border-zinc-300" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 h-full">
|
||||||
|
{tutorials.map((tutorial, tutorialIndex) => (
|
||||||
|
<button
|
||||||
|
key={tutorialIndex}
|
||||||
|
onClick={() => goToTutorial(tutorial.title)}
|
||||||
|
aria-label={tutorial.title + " tutorial"}
|
||||||
|
className="flex flex-col justify-center items-center bg-zinc-50 rounded-xl p-4 shadow-md border-2 border-zinc-300 cursor-pointer text-center text-sm transition hover:scale-[1.03] hover:bg-cyan-100 hover:border-cyan-600"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={tutorial.thumbnail!}
|
||||||
|
alt="tutorial thumbnail"
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
className="rounded-lg border-2 border-zinc-300"
|
||||||
|
/>
|
||||||
|
<p className="mt-2">{tutorial.title}</p>
|
||||||
|
{/* Set opacity to 0 to keep height the same with other tutorials */}
|
||||||
|
<p className={`text-[0.65rem] text-zinc-400 ${!tutorial.hint && "opacity-0"}`}>{tutorial.hint || "placeholder"}</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : slide.type === "finish" ? (
|
||||||
|
<div className="h-full flex flex-col justify-center items-center">
|
||||||
|
<Icon icon="fxemoji:partypopper" className="text-9xl" />
|
||||||
|
<h1 className="font-medium text-xl mt-6 animate-bounce">Yatta! You did it!</h1>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-zinc-500 mb-2 text-center">{slide.text}</p>
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src={slide.imageSrc ?? "/missing.svg"}
|
||||||
|
alt="step image"
|
||||||
|
width={396}
|
||||||
|
height={320}
|
||||||
|
loading="eager"
|
||||||
|
className="rounded-lg w-full h-full object-contain bg-black flex-1"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Arrows */}
|
||||||
|
<div className={`flex justify-between items-center mt-2 px-6 pb-6 transition-opacity duration-300 ${isStartingPage && "opacity-0"}`}>
|
||||||
|
<button
|
||||||
|
onClick={() => emblaApi?.scrollPrev()}
|
||||||
|
disabled={isStartingPage}
|
||||||
|
className={`pill button p-1! aspect-square text-2xl ${isStartingPage && "cursor-auto!"}`}
|
||||||
|
aria-label="Scroll Carousel Left"
|
||||||
|
>
|
||||||
|
<Icon icon="tabler:chevron-left" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Only show tutorial name on step slides */}
|
||||||
|
<span
|
||||||
|
className={`text-sm transition-opacity duration-300 ${
|
||||||
|
(currentSlide.type === "finish" || currentSlide.type === "start") && "opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{currentSlide?.tutorialTitle}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => emblaApi?.scrollNext()}
|
||||||
|
disabled={isStartingPage}
|
||||||
|
className={`pill button p-1! aspect-square text-2xl ${isStartingPage && "cursor-auto!"}`}
|
||||||
|
aria-label="Scroll Carousel Right"
|
||||||
|
>
|
||||||
|
<Icon icon="tabler:chevron-right" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
"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 (
|
|
||||||
<div className="shrink-0 flex flex-col w-full px-6">
|
|
||||||
{!finishIndex ? (
|
|
||||||
<>
|
|
||||||
<p className="text-sm text-zinc-500 mb-2 text-center">{text}</p>
|
|
||||||
|
|
||||||
<Image
|
|
||||||
src={imageSrc ?? "/missing.svg"}
|
|
||||||
alt="step image"
|
|
||||||
width={396}
|
|
||||||
height={320}
|
|
||||||
className="rounded-lg w-full h-full object-contain bg-black flex-1"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="h-full flex flex-col justify-center items-center">
|
|
||||||
<Icon icon="fxemoji:partypopper" className="text-9xl" />
|
|
||||||
<h1 className="font-medium text-xl mt-6 animate-bounce">Yatta! You did it!</h1>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import useEmblaCarousel from "embla-carousel-react";
|
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
import TutorialPage from "./page";
|
import Tutorial from ".";
|
||||||
|
|
||||||
export default function ScanTutorialButton() {
|
export default function ScanTutorialButton() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -43,60 +18,23 @@ export default function ScanTutorialButton() {
|
||||||
|
|
||||||
{isOpen &&
|
{isOpen &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div className="fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40">
|
<Tutorial
|
||||||
<div
|
tutorials={[
|
||||||
onClick={close}
|
{
|
||||||
className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${
|
title: "Adding Mii",
|
||||||
isVisible ? "opacity-100" : "opacity-0"
|
steps: [
|
||||||
}`}
|
{ text: "1. Enter the town hall", imageSrc: "/tutorial/step1.png" },
|
||||||
/>
|
{ text: "2. Go into 'QR Code'", imageSrc: "/tutorial/adding-mii/step2.png" },
|
||||||
|
{ text: "3. Press 'Scan QR Code'", imageSrc: "/tutorial/adding-mii/step3.png" },
|
||||||
<div
|
{ text: "4. Click on the QR code below the Mii's image", imageSrc: "/tutorial/adding-mii/step4.png" },
|
||||||
className={`z-50 bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg w-full max-w-md h-120 transition-discrete duration-300 flex flex-col ${
|
{ text: "5. Scan with your 3DS", imageSrc: "/tutorial/adding-mii/step5.png" },
|
||||||
isVisible ? "scale-100 opacity-100" : "scale-75 opacity-0"
|
{ type: "finish" },
|
||||||
}`}
|
],
|
||||||
>
|
},
|
||||||
<div className="flex justify-between items-center mb-2 p-6 pb-0">
|
]}
|
||||||
<h2 className="text-xl font-bold">Tutorial</h2>
|
isOpen={isOpen}
|
||||||
<button onClick={close} aria-label="Close" className="text-red-400 hover:text-red-500 text-2xl cursor-pointer">
|
setIsOpen={setIsOpen}
|
||||||
<Icon icon="material-symbols:close-rounded" />
|
/>,
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col min-h-0 h-full">
|
|
||||||
<div className="overflow-hidden h-full" ref={emblaRef}>
|
|
||||||
<div className="flex h-full">
|
|
||||||
<TutorialPage text="1. Enter the town hall" imageSrc="/tutorial/step1.png" />
|
|
||||||
<TutorialPage text="2. Go into 'QR Code'" imageSrc="/tutorial/adding-mii/step2.png" />
|
|
||||||
<TutorialPage text="3. Press 'Scan QR Code'" imageSrc="/tutorial/adding-mii/step3.png" />
|
|
||||||
<TutorialPage text="4. Click on the QR code below the Mii's image" imageSrc="/tutorial/adding-mii/step4.png" />
|
|
||||||
<TutorialPage text="5. Scan with your 3DS" imageSrc="/tutorial/adding-mii/step5.png" />
|
|
||||||
<TutorialPage carouselIndex={selectedIndex} finishIndex={5} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center mt-2 px-6 pb-6">
|
|
||||||
<button
|
|
||||||
onClick={() => emblaApi?.scrollPrev()}
|
|
||||||
aria-label="Scroll Carousel Left"
|
|
||||||
className="pill button p-1! aspect-square text-2xl"
|
|
||||||
>
|
|
||||||
<Icon icon="tabler:chevron-left" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span className="text-sm">Adding Mii to Island</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => emblaApi?.scrollNext()}
|
|
||||||
aria-label="Scroll Carousel Right"
|
|
||||||
className="pill button p-1! aspect-square text-2xl"
|
|
||||||
>
|
|
||||||
<Icon icon="tabler:chevron-right" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
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 (
|
|
||||||
<div className="shrink-0 flex flex-col w-full px-6 py-6">
|
|
||||||
{/* Separator */}
|
|
||||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2">
|
|
||||||
<hr className="grow border-zinc-300" />
|
|
||||||
<span>Pick a tutorial</span>
|
|
||||||
<hr className="grow border-zinc-300" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 h-full">
|
|
||||||
<button
|
|
||||||
onClick={() => goToTutorial(1)}
|
|
||||||
aria-label="Allow Copying Tutorial"
|
|
||||||
className="flex flex-col justify-center items-center bg-zinc-50 rounded-xl p-4 shadow-md border-2 border-zinc-300 cursor-pointer text-center text-sm transition hover:scale-[1.03] hover:bg-cyan-100 hover:border-cyan-600"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={"/tutorial/allow-copying/thumbnail.png"}
|
|
||||||
alt="Allow Copying thumbnail"
|
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
className="rounded-lg border-2 border-zinc-300"
|
|
||||||
/>
|
|
||||||
<p className="mt-2">Allow Copying</p>
|
|
||||||
<p className="text-[0.65rem] text-zinc-400">Suggested!</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => goToTutorial(10)}
|
|
||||||
aria-label="Create QR Code Tutorial"
|
|
||||||
className="flex flex-col justify-center items-center bg-zinc-50 rounded-xl p-4 shadow-md border-2 border-zinc-300 cursor-pointer text-center text-sm transition hover:scale-[1.03] hover:bg-cyan-100 hover:border-cyan-600"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={"/tutorial/create-qr-code/thumbnail.png"}
|
|
||||||
alt="Creating QR code thumbnail"
|
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
className="rounded-lg border-2 border-zinc-300"
|
|
||||||
/>
|
|
||||||
<p className="mt-2">Create QR Code</p>
|
|
||||||
{/* Add placeholder to keep height the same */}
|
|
||||||
<p className="text-[0.65rem] opacity-0">placeholder</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import useEmblaCarousel from "embla-carousel-react";
|
import Tutorial from ".";
|
||||||
import { Icon } from "@iconify/react";
|
|
||||||
|
|
||||||
import TutorialPage from "./page";
|
|
||||||
import StartingPage from "./starting-page";
|
|
||||||
|
|
||||||
export default function SubmitTutorialButton() {
|
export default function SubmitTutorialButton() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -46,84 +15,48 @@ export default function SubmitTutorialButton() {
|
||||||
|
|
||||||
{isOpen &&
|
{isOpen &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div className="fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40">
|
<Tutorial
|
||||||
<div
|
tutorials={[
|
||||||
onClick={close}
|
{
|
||||||
className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${
|
title: "Allow Copying",
|
||||||
isVisible ? "opacity-100" : "opacity-0"
|
thumbnail: "/tutorial/allow-copying/thumbnail.png",
|
||||||
}`}
|
hint: "Suggested!",
|
||||||
/>
|
steps: [
|
||||||
|
{ type: "start" },
|
||||||
<div
|
{ text: "1. Enter the town hall", imageSrc: "/tutorial/step1.png" },
|
||||||
className={`z-50 bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg w-full max-w-md h-120 transition-discrete duration-300 flex flex-col ${
|
{ text: "2. Go into 'Mii List'", imageSrc: "/tutorial/allow-copying/step2.png" },
|
||||||
isVisible ? "scale-100 opacity-100" : "scale-75 opacity-0"
|
{ text: "3. Select and edit the Mii you wish to submit", imageSrc: "/tutorial/allow-copying/step3.png" },
|
||||||
}`}
|
{ text: "4. Click 'Other Settings' in the information screen", imageSrc: "/tutorial/allow-copying/step4.png" },
|
||||||
>
|
{ text: "5. Click on 'Don't Allow' under the 'Copying' text", imageSrc: "/tutorial/allow-copying/step5.png" },
|
||||||
<div className="flex justify-between items-center mb-2 p-6 pb-0">
|
{ text: "6. Press 'Allow'", imageSrc: "/tutorial/allow-copying/step6.png" },
|
||||||
<h2 className="text-xl font-bold">Tutorial</h2>
|
{ text: "7. Confirm the edits to the Mii", imageSrc: "/tutorial/allow-copying/step7.png" },
|
||||||
<button onClick={close} aria-label="Close" className="text-red-400 hover:text-red-500 text-2xl cursor-pointer">
|
{ type: "finish" },
|
||||||
<Icon icon="material-symbols:close-rounded" />
|
],
|
||||||
</button>
|
},
|
||||||
</div>
|
{
|
||||||
|
title: "Create QR Code",
|
||||||
<div className="flex flex-col min-h-0 h-full">
|
thumbnail: "/tutorial/create-qr-code/thumbnail.png",
|
||||||
<div className="overflow-hidden h-full" ref={emblaRef}>
|
steps: [
|
||||||
<div className="flex h-full">
|
{ type: "start" },
|
||||||
<StartingPage emblaApi={emblaApi} />
|
{ text: "1. Enter the town hall", imageSrc: "/tutorial/step1.png" },
|
||||||
|
{ text: "2. Go into 'QR Code'", imageSrc: "/tutorial/create-qr-code/step2.png" },
|
||||||
{/* Allow Copying */}
|
{ text: "3. Press 'Create QR Code'", imageSrc: "/tutorial/create-qr-code/step3.png" },
|
||||||
<TutorialPage text="1. Enter the town hall" imageSrc="/tutorial/step1.png" />
|
{ text: "4. Select and press 'OK' on the Mii you wish to submit", imageSrc: "/tutorial/create-qr-code/step4.png" },
|
||||||
<TutorialPage text="2. Go into 'Mii List'" imageSrc="/tutorial/allow-copying/step2.png" />
|
{
|
||||||
<TutorialPage text="3. Select and edit the Mii you wish to submit" imageSrc="/tutorial/allow-copying/step3.png" />
|
text: "5. Pick any option; it doesn't matter since the QR code regenerates upon submission.",
|
||||||
<TutorialPage text="4. Click 'Other Settings' in the information screen" imageSrc="/tutorial/allow-copying/step4.png" />
|
imageSrc: "/tutorial/create-qr-code/step5.png",
|
||||||
<TutorialPage text="5. Click on 'Don't Allow' under the 'Copying' text" imageSrc="/tutorial/allow-copying/step5.png" />
|
},
|
||||||
<TutorialPage text="6. Press 'Allow'" imageSrc="/tutorial/allow-copying/step6.png" />
|
{
|
||||||
<TutorialPage text="7. Confirm the edits to the Mii" imageSrc="/tutorial/allow-copying/step7.png" />
|
text: "6. Exit the tutorial; Upload the QR code (scan with camera or upload file through SD card).",
|
||||||
<TutorialPage carouselIndex={selectedIndex} finishIndex={8} />
|
imageSrc: "/tutorial/create-qr-code/step6.png",
|
||||||
|
},
|
||||||
<StartingPage emblaApi={emblaApi} />
|
{ type: "finish" },
|
||||||
|
],
|
||||||
{/* Create QR Code */}
|
},
|
||||||
<TutorialPage text="1. Enter the town hall" imageSrc="/tutorial/step1.png" />
|
]}
|
||||||
<TutorialPage text="2. Go into 'QR Code'" imageSrc="/tutorial/create-qr-code/step2.png" />
|
isOpen={isOpen}
|
||||||
<TutorialPage text="3. Press 'Create QR Code'" imageSrc="/tutorial/create-qr-code/step3.png" />
|
setIsOpen={setIsOpen}
|
||||||
<TutorialPage text="4. Select and press 'OK' on the Mii you wish to submit" imageSrc="/tutorial/create-qr-code/step4.png" />
|
/>,
|
||||||
<TutorialPage
|
|
||||||
text="5. Pick any option; it doesn't matter since the QR code regenerates upon submission."
|
|
||||||
imageSrc="/tutorial/create-qr-code/step5.png"
|
|
||||||
/>
|
|
||||||
<TutorialPage
|
|
||||||
text="6. Exit the tutorial; Upload the QR code (scan with camera or upload file through SD card)."
|
|
||||||
imageSrc="/tutorial/create-qr-code/step6.png"
|
|
||||||
/>
|
|
||||||
<TutorialPage carouselIndex={selectedIndex} finishIndex={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`flex justify-between items-center mt-2 px-6 pb-6 transition-opacity duration-300 ${isStartingPage && "opacity-0"}`}>
|
|
||||||
<button
|
|
||||||
onClick={() => emblaApi?.scrollPrev()}
|
|
||||||
disabled={isStartingPage}
|
|
||||||
className={`pill button p-1! aspect-square text-2xl ${isStartingPage && "cursor-auto!"}`}
|
|
||||||
aria-label="Scroll Carousel Left"
|
|
||||||
>
|
|
||||||
<Icon icon="tabler:chevron-left" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span className="text-sm">{inTutorialAllowCopying ? "Allow Copying" : "Create QR Code"}</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => emblaApi?.scrollNext()}
|
|
||||||
disabled={isStartingPage}
|
|
||||||
className={`pill button p-1! aspect-square text-2xl ${isStartingPage && "cursor-auto!"}`}
|
|
||||||
aria-label="Scroll Carousel Right"
|
|
||||||
>
|
|
||||||
<Icon icon="tabler:chevron-right" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue