feat: 20f1c51f access key version
This commit is contained in:
parent
76fecca011
commit
07f3bb35d8
10 changed files with 907 additions and 320 deletions
122
package.json
122
package.json
|
|
@ -1,60 +1,66 @@
|
|||
{
|
||||
"name": "tomodachi-share",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.14.0",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"postinstall": "prisma generate",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@2toad/profanity": "^3.1.1",
|
||||
"@auth/prisma-adapter": "2.10.0",
|
||||
"@bprogress/next": "^3.2.12",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@prisma/client": "^6.16.1",
|
||||
"bit-buffer": "^0.2.5",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"dayjs": "^1.11.18",
|
||||
"downshift": "^9.0.10",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-type": "^21.0.0",
|
||||
"ioredis": "^5.7.0",
|
||||
"jsqr": "^1.4.0",
|
||||
"next": "15.5.3",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"qrcode-generator": "^2.0.4",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-webcam": "^7.2.0",
|
||||
"satori": "^0.18.2",
|
||||
"seedrandom": "^3.0.5",
|
||||
"sharp": "^0.34.3",
|
||||
"sjcl-with-all": "1.0.8",
|
||||
"swr": "^2.3.6",
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@iconify/react": "^6.0.1",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"@types/sjcl": "^1.0.34",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-config-next": "15.5.3",
|
||||
"prisma": "^6.16.1",
|
||||
"schema-dts": "^1.1.5",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
"name": "tomodachi-share",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.14.0",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"postinstall": "prisma generate",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@2toad/profanity": "^3.1.1",
|
||||
"@auth/prisma-adapter": "2.10.0",
|
||||
"@bprogress/next": "^3.2.12",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@prisma/client": "^6.16.1",
|
||||
"bit-buffer": "^0.2.5",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"dayjs": "^1.11.18",
|
||||
"downshift": "^9.0.10",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-type": "^21.0.0",
|
||||
"ioredis": "^5.7.0",
|
||||
"jsqr": "^1.4.0",
|
||||
"next": "16.0.0-beta.0",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"qrcode-generator": "^2.0.4",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-webcam": "^7.2.0",
|
||||
"satori": "^0.18.2",
|
||||
"seedrandom": "^3.0.5",
|
||||
"sharp": "^0.34.3",
|
||||
"sjcl-with-all": "1.0.8",
|
||||
"swr": "^2.3.6",
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@iconify/react": "^6.0.1",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.1",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"@types/sjcl": "^1.0.34",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-config-next": "16.0.0-beta.0",
|
||||
"prisma": "^6.16.1",
|
||||
"schema-dts": "^1.1.5",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
817
pnpm-lock.yaml
817
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "public"."miis" ADD COLUMN "accessKey" VARCHAR(7);
|
||||
|
|
@ -76,6 +76,7 @@ model Mii {
|
|||
tags String[]
|
||||
description String? @db.VarChar(256)
|
||||
platform MiiPlatform @default(THREE_DS)
|
||||
accessKey String? @db.VarChar(7)
|
||||
|
||||
firstName String?
|
||||
lastName String?
|
||||
|
|
|
|||
|
|
@ -29,11 +29,18 @@ const submitSchema = z
|
|||
description: z.string().trim().max(256).optional(),
|
||||
|
||||
// Switch
|
||||
accessKey: z
|
||||
.string()
|
||||
.length(7, { error: "Access key must be 7 characters in length" })
|
||||
.regex(/^[a-zA-Z0-9]+$/, "Access key must be alphanumeric"),
|
||||
gender: z.enum(MiiGender).default("MALE"),
|
||||
miiPortraitImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||
|
||||
// QR code
|
||||
qrBytesRaw: z.array(z.number(), { error: "A QR code is required" }).length(372, { error: "QR code size is not a valid Tomodachi Life QR code" }),
|
||||
qrBytesRaw: z
|
||||
.array(z.number(), { error: "A QR code is required" })
|
||||
.length(372, { error: "QR code size is not a valid Tomodachi Life QR code" })
|
||||
.optional(),
|
||||
|
||||
// Custom images
|
||||
image1: z.union([z.instanceof(File), z.any()]).optional(),
|
||||
|
|
@ -42,15 +49,15 @@ const submitSchema = z
|
|||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// If platform is Switch, gender and miiPortraitImage must be present
|
||||
// If platform is Switch, accessKey, gender, and miiPortraitImage must be present
|
||||
if (data.platform === "SWITCH") {
|
||||
return data.gender !== undefined && data.miiPortraitImage !== undefined;
|
||||
return data.accessKey !== undefined && data.gender !== undefined && data.miiPortraitImage !== undefined;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Gender and Mii portrait image are required for Switch platform",
|
||||
path: ["gender", "miiPortraitImage"],
|
||||
message: "Access key, gender, and Mii portrait image is required for Switch",
|
||||
path: ["accessKey", "gender", "miiPortraitImage"],
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -70,7 +77,7 @@ export async function POST(request: NextRequest) {
|
|||
const formData = await request.formData();
|
||||
|
||||
let rawTags: string[];
|
||||
let rawQrBytesRaw: string[]; // raw raw
|
||||
let rawQrBytesRaw: string[] | undefined = undefined; // good variable name - raw raw; is undefined for zod to ignore it if platform is Switch
|
||||
try {
|
||||
rawTags = JSON.parse(formData.get("tags") as string);
|
||||
rawQrBytesRaw = JSON.parse(formData.get("qrBytesRaw") as string);
|
||||
|
|
@ -85,10 +92,11 @@ export async function POST(request: NextRequest) {
|
|||
tags: rawTags,
|
||||
description: formData.get("description"),
|
||||
|
||||
gender: formData.get("gender") ?? undefined, // ZOD MOMENT
|
||||
accessKey: formData.get("accessKey"),
|
||||
gender: formData.get("gender"),
|
||||
miiPortraitImage: formData.get("miiPortraitImage"),
|
||||
|
||||
qrBytesRaw: rawQrBytesRaw,
|
||||
qrBytesRaw: rawQrBytesRaw ?? undefined,
|
||||
|
||||
image1: formData.get("image1"),
|
||||
image2: formData.get("image2"),
|
||||
|
|
@ -123,7 +131,7 @@ export async function POST(request: NextRequest) {
|
|||
if (!imageValidation.valid) return rateLimit.sendResponse({ error: imageValidation.error }, imageValidation.status ?? 400);
|
||||
}
|
||||
|
||||
const qrBytes = new Uint8Array(data.qrBytesRaw);
|
||||
const qrBytes = new Uint8Array(data.qrBytesRaw ?? []);
|
||||
|
||||
// Convert QR code to JS (3DS)
|
||||
let conversion: { mii: Mii; tomodachiLifeMii: TomodachiLifeMii } | undefined;
|
||||
|
|
@ -145,6 +153,9 @@ export async function POST(request: NextRequest) {
|
|||
description,
|
||||
gender: data.gender ?? "MALE",
|
||||
|
||||
// Access key only for Switch
|
||||
accessKey: data.platform === "SWITCH" ? data.accessKey : null,
|
||||
|
||||
// Automatically detect certain information if on 3DS
|
||||
...(data.platform === "THREE_DS" &&
|
||||
conversion && {
|
||||
|
|
@ -191,29 +202,31 @@ export async function POST(request: NextRequest) {
|
|||
return rateLimit.sendResponse({ error: "Failed to download/store Mii portrait" }, 500);
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate a new QR code for aesthetic reasons
|
||||
const byteString = String.fromCharCode(...qrBytes);
|
||||
const generatedCode = qrcode(0, "L");
|
||||
generatedCode.addData(byteString, "Byte");
|
||||
generatedCode.make();
|
||||
if (data.platform === "THREE_DS") {
|
||||
try {
|
||||
// Generate a new QR code for aesthetic reasons
|
||||
const byteString = String.fromCharCode(...qrBytes);
|
||||
const generatedCode = qrcode(0, "L");
|
||||
generatedCode.addData(byteString, "Byte");
|
||||
generatedCode.make();
|
||||
|
||||
// Store QR code
|
||||
const codeDataUrl = generatedCode.createDataURL();
|
||||
const codeBase64 = codeDataUrl.replace(/^data:image\/gif;base64,/, "");
|
||||
const codeBuffer = Buffer.from(codeBase64, "base64");
|
||||
// Store QR code
|
||||
const codeDataUrl = generatedCode.createDataURL();
|
||||
const codeBase64 = codeDataUrl.replace(/^data:image\/gif;base64,/, "");
|
||||
const codeBuffer = Buffer.from(codeBase64, "base64");
|
||||
|
||||
// Compress and store
|
||||
const codeWebpBuffer = await sharp(codeBuffer).webp({ quality: 85 }).toBuffer();
|
||||
const codeFileLocation = path.join(miiUploadsDirectory, "qr-code.webp");
|
||||
// Compress and store
|
||||
const codeWebpBuffer = await sharp(codeBuffer).webp({ quality: 85 }).toBuffer();
|
||||
const codeFileLocation = path.join(miiUploadsDirectory, "qr-code.webp");
|
||||
|
||||
await fs.writeFile(codeFileLocation, codeWebpBuffer);
|
||||
} catch (error) {
|
||||
// Clean up if something went wrong
|
||||
await prisma.mii.delete({ where: { id: miiRecord.id } });
|
||||
await fs.writeFile(codeFileLocation, codeWebpBuffer);
|
||||
} catch (error) {
|
||||
// Clean up if something went wrong
|
||||
await prisma.mii.delete({ where: { id: miiRecord.id } });
|
||||
|
||||
console.error("Error generating QR code:", error);
|
||||
return rateLimit.sendResponse({ error: "Failed to generate QR code" }, 500);
|
||||
console.error("Error generating QR code:", error);
|
||||
return rateLimit.sendResponse({ error: "Failed to generate QR code" }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -119,15 +119,19 @@ export default async function MiiPage({ params }: Props) {
|
|||
className="drop-shadow-lg hover:scale-105 transition-transform duration-300 object-contain size-full"
|
||||
/>
|
||||
</div>
|
||||
{/* QR Code */}
|
||||
{/* QR Code/Access key */}
|
||||
<div className="bg-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center p-2">
|
||||
<ImageViewer
|
||||
src={`/mii/${mii.id}/image?type=qr-code`}
|
||||
alt="mii qr code"
|
||||
width={128}
|
||||
height={128}
|
||||
className="border-2 border-amber-300 rounded-lg hover:brightness-90 transition-all"
|
||||
/>
|
||||
{mii.platform === "THREE_DS" ? (
|
||||
<ImageViewer
|
||||
src={`/mii/${mii.id}/image?type=qr-code`}
|
||||
alt="mii qr code"
|
||||
width={128}
|
||||
height={128}
|
||||
className="border-2 border-amber-300 rounded-lg hover:brightness-90 transition-all"
|
||||
/>
|
||||
) : (
|
||||
<h1 className="font-bold text-3xl">{mii.accessKey}</h1>
|
||||
)}
|
||||
</div>
|
||||
<hr className="w-full border-t-2 border-t-amber-400" />
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
<Carousel
|
||||
images={[
|
||||
`/mii/${mii.id}/image?type=mii`,
|
||||
`/mii/${mii.id}/image?type=qr-code`,
|
||||
...(mii.platform === "THREE_DS" ? [`/mii/${mii.id}/image?type=qr-code`] : []),
|
||||
...Array.from({ length: mii.imageCount }, (_, index) => `/mii/${mii.id}/image?type=image${index}`),
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default function SubmitForm() {
|
|||
const [name, setName] = useState("");
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [description, setDescription] = useState("");
|
||||
const [accessKey, setAccessKey] = useState("");
|
||||
const [gender, setGender] = useState<MiiGender>("MALE");
|
||||
const [qrBytesRaw, setQrBytesRaw] = useState<number[]>([]);
|
||||
|
||||
|
|
@ -67,16 +68,18 @@ export default function SubmitForm() {
|
|||
formData.append("name", name);
|
||||
formData.append("tags", JSON.stringify(tags));
|
||||
formData.append("description", description);
|
||||
formData.append("qrBytesRaw", JSON.stringify(qrBytesRaw));
|
||||
files.forEach((file, index) => {
|
||||
// image1, image2, etc.
|
||||
formData.append(`image${index + 1}`, file);
|
||||
});
|
||||
|
||||
if (platform === "SWITCH") {
|
||||
if (platform === "THREE_DS") {
|
||||
formData.append("qrBytesRaw", JSON.stringify(qrBytesRaw));
|
||||
} else if (platform === "SWITCH") {
|
||||
const response = await fetch(miiPortraitUri!);
|
||||
const blob = await response.blob();
|
||||
|
||||
formData.append("accessKey", accessKey);
|
||||
formData.append("gender", gender);
|
||||
formData.append("miiPortraitImage", blob);
|
||||
}
|
||||
|
|
@ -96,6 +99,7 @@ export default function SubmitForm() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (platform !== "THREE_DS") return;
|
||||
if (qrBytesRaw.length == 0) return;
|
||||
const qrBytes = new Uint8Array(qrBytesRaw);
|
||||
|
||||
|
|
@ -108,16 +112,14 @@ export default function SubmitForm() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Convert QR code to JS (3DS)
|
||||
if (platform === "THREE_DS") {
|
||||
let conversion: { mii: Mii; tomodachiLifeMii: TomodachiLifeMii };
|
||||
try {
|
||||
conversion = convertQrCode(qrBytes);
|
||||
setMiiPortraitUri(conversion.mii.studioUrl({ width: 512 }));
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : String(error));
|
||||
return;
|
||||
}
|
||||
// Convert QR code to JS
|
||||
let conversion: { mii: Mii; tomodachiLifeMii: TomodachiLifeMii };
|
||||
try {
|
||||
conversion = convertQrCode(qrBytes);
|
||||
setMiiPortraitUri(conversion.mii.studioUrl({ width: 512 }));
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : String(error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a new QR code for aesthetic reasons
|
||||
|
|
@ -256,41 +258,56 @@ export default function SubmitForm() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Gender (switch only) */}
|
||||
{platform === "SWITCH" && (
|
||||
<div className="w-full grid grid-cols-3 items-start">
|
||||
<label htmlFor="gender" className="font-semibold py-2">
|
||||
Gender
|
||||
</label>
|
||||
<div className="col-span-2 flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGender("MALE")}
|
||||
aria-label="Filter for Male Miis"
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all ${
|
||||
gender === "MALE" ? "bg-blue-100 border-blue-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
>
|
||||
<Icon icon="foundation:male" className="text-blue-400" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGender("FEMALE")}
|
||||
aria-label="Filter for Female Miis"
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all ${
|
||||
gender === "FEMALE" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
>
|
||||
<Icon icon="foundation:female" className="text-pink-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{platform === "SWITCH" && (
|
||||
<>
|
||||
{/* Separator */}
|
||||
{/* Access Key */}
|
||||
<div className="w-full grid grid-cols-3 items-center">
|
||||
<label htmlFor="accessKey" className="font-semibold">
|
||||
Access Key <SwitchSubmitTutorialButton />
|
||||
</label>
|
||||
<input
|
||||
name="accessKey"
|
||||
type="text"
|
||||
className="pill input w-full col-span-2"
|
||||
minLength={7}
|
||||
maxLength={7}
|
||||
placeholder="Type your mii's access key here..."
|
||||
value={accessKey}
|
||||
onChange={(e) => setAccessKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gender */}
|
||||
<div className="w-full grid grid-cols-3 items-start">
|
||||
<label htmlFor="gender" className="font-semibold py-2">
|
||||
Gender
|
||||
</label>
|
||||
<div className="col-span-2 flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGender("MALE")}
|
||||
aria-label="Filter for Male Miis"
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all ${
|
||||
gender === "MALE" ? "bg-blue-100 border-blue-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
>
|
||||
<Icon icon="foundation:male" className="text-blue-400" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGender("FEMALE")}
|
||||
aria-label="Filter for Female Miis"
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all ${
|
||||
gender === "FEMALE" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
>
|
||||
<Icon icon="foundation:female" className="text-pink-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mii Portrait */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2">
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
<span>Mii Portrait</span>
|
||||
|
|
@ -304,27 +321,24 @@ export default function SubmitForm() {
|
|||
)}
|
||||
|
||||
{/* QR code selector */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2">
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
<span>QR Code</span>
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
</div>
|
||||
{platform === "THREE_DS" && (
|
||||
<>
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2">
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
<span>QR Code</span>
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<QrUpload setQrBytesRaw={setQrBytesRaw} />
|
||||
<span>or</span>
|
||||
<QrScanner setQrBytesRaw={setQrBytesRaw} />
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<QrUpload setQrBytesRaw={setQrBytesRaw} />
|
||||
<span>or</span>
|
||||
<QrScanner setQrBytesRaw={setQrBytesRaw} />
|
||||
|
||||
{platform === "THREE_DS" ? (
|
||||
<>
|
||||
<ThreeDsSubmitTutorialButton />
|
||||
|
||||
<span className="text-xs text-zinc-400">For emulators, aes_keys.txt is required.</span>
|
||||
</>
|
||||
) : (
|
||||
<SwitchSubmitTutorialButton />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Custom images selector */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-6 mb-2">
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ export default function ThreeDsSubmitTutorialButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => setIsOpen(true)} className="text-sm text-orange-400 cursor-pointer underline-offset-2 hover:underline">
|
||||
How to?
|
||||
<button type="button" onClick={() => setIsOpen(true)} className="text-orange-400 cursor-pointer underline-offset-2 hover:underline">
|
||||
(?)
|
||||
</button>
|
||||
|
||||
{isOpen &&
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"sjcl-with-all": ["./node_modules/@types/sjcl"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"sjcl-with-all": ["./node_modules/@types/sjcl"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue