Compare commits

..

No commits in common. "971f0c3c00654d8680df7652d46f932c264d3fcf" and "8378b851677b784216a162549665df71810cb8c9" have entirely different histories.

8 changed files with 34 additions and 76 deletions

View file

@ -93,14 +93,14 @@ export default function RootLayout({
)}
<Providers>
<SessionProvider>
<Suspense fallback={<div>Loading header...</div>}>
<Suspense fallback={<div>Loading header...</div>}>
<SessionProvider>
<Header />
</Suspense>
<AdminBanner />
<main className="px-4 py-8 max-w-7xl w-full grow flex flex-col">{children}</main>
<Footer />
</SessionProvider>
</SessionProvider>
</Suspense>
<AdminBanner />
<main className="px-4 py-8 max-w-7xl w-full grow flex flex-col">{children}</main>
<Footer />
</Providers>
</body>
</html>

View file

@ -316,7 +316,7 @@ export default async function MiiPage({ params }: Props) {
{/* Submission name */}
<h1 className="text-4xl font-extrabold wrap-break-word text-amber-700">{mii.name}</h1>
{/* Like button */}
<LikeButton likes={mii._count.likedBy ?? 0} miiId={mii.id} isLiked={(mii.likedBy ?? []).length > 0} big />
<LikeButton likes={mii._count.likedBy ?? 0} miiId={mii.id} isLiked={(mii.likedBy ?? []).length > 0} isLoggedIn={session?.user != null} big />
</div>
{/* Tags */}
<div id="tags" className="flex flex-wrap gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">

View file

@ -1,8 +1,6 @@
import { redirect } from "next/navigation";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
export default async function RandomPage() {
const count = await prisma.mii.count();
if (count === 0) redirect("/");

View file

@ -1,6 +1,5 @@
"use client";
import { useSession } from "next-auth/react";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Icon, loadIcons } from "@iconify/react";
@ -10,13 +9,13 @@ interface Props {
likes: number;
miiId?: number | undefined;
isLiked: boolean;
isLoggedIn?: boolean;
disabled?: boolean;
abbreviate?: boolean;
big?: boolean;
}
export default function LikeButton({ likes, isLiked, miiId, disabled, abbreviate, big }: Props) {
const session = useSession();
export default function LikeButton({ likes, isLiked, miiId, isLoggedIn, disabled, abbreviate, big }: Props) {
const router = useRouter();
const [isLikedState, setIsLikedState] = useState(isLiked);
@ -25,7 +24,7 @@ export default function LikeButton({ likes, isLiked, miiId, disabled, abbreviate
const onClick = async () => {
if (disabled) return;
if (!session.data?.user) {
if (!isLoggedIn) {
router.push("/login");
return;
}

View file

@ -224,7 +224,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
</div>
<div className="mt-auto grid grid-cols-2 items-center">
<LikeButton likes={mii.likes} miiId={mii.id} isLiked={mii.isLiked} abbreviate />
<LikeButton likes={mii.likes} miiId={mii.id} isLiked={mii.isLiked} isLoggedIn={session?.user != null} abbreviate />
{!userId && (
<Link href={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis">

View file

@ -10,12 +10,11 @@ import { useSelect } from "downshift";
interface Props {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
onCapture?: () => void;
setImage?: React.Dispatch<React.SetStateAction<string | undefined>>;
setQrBytesRaw?: React.Dispatch<React.SetStateAction<number[]>>;
}
export default function Camera({ isOpen, setIsOpen, onCapture, setImage, setQrBytesRaw }: Props) {
export default function Camera({ isOpen, setIsOpen, setImage, setQrBytesRaw }: Props) {
const [isVisible, setIsVisible] = useState(false);
const [permissionGranted, setPermissionGranted] = useState<boolean | null>(null);
@ -24,7 +23,6 @@ export default function Camera({ isOpen, setIsOpen, onCapture, setImage, setQrBy
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const streamRef = useRef<MediaStream | null>(null);
const requestRef = useRef<number>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
@ -67,7 +65,6 @@ export default function Camera({ isOpen, setIsOpen, onCapture, setImage, setQrBy
if (setImage) {
setImage(canvas.toDataURL());
if (onCapture) onCapture();
close();
return;
}
@ -92,34 +89,14 @@ export default function Camera({ isOpen, setIsOpen, onCapture, setImage, setQrBy
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
// immediately stop this temp stream
stream.getTracks().forEach((track) => track.stop());
setPermissionGranted(true);
})
.then(() => setPermissionGranted(true))
.catch((err) => {
setPermissionGranted(false);
console.error("An error occurred trying to access the camera", err);
});
};
const stopCamera = () => {
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
requestRef.current = null;
}
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.srcObject = null;
}
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop());
streamRef.current = null;
}
};
const close = () => {
stopCamera();
setIsVisible(false);
setTimeout(() => {
setIsOpen(false);
@ -155,7 +132,6 @@ export default function Camera({ isOpen, setIsOpen, onCapture, setImage, setQrBy
})
.then((stream) => {
if (!stream || !videoRef.current) return;
streamRef.current = stream;
videoRef.current.srcObject = stream;
videoRef.current.play();
})
@ -165,9 +141,16 @@ export default function Camera({ isOpen, setIsOpen, onCapture, setImage, setQrBy
// cleanup
return () => {
stopCamera();
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
}
if (videoRef.current?.srcObject) {
const stream = videoRef.current.srcObject as MediaStream;
stream.getTracks().forEach((track) => track.stop());
videoRef.current.srcObject = null;
}
};
}, [isOpen, permissionGranted, selectedDeviceId]);
}, [isOpen, permissionGranted, selectedDeviceId, takePicture]);
return (
<div className={`fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40 ${!isOpen ? "hidden" : ""}`}>
@ -235,10 +218,7 @@ export default function Camera({ isOpen, setIsOpen, onCapture, setImage, setQrBy
</div>
)}
<div className="rounded-2xl border-2 border-amber-500 max-h-96 flex justify-center items-center overflow-hidden">
<img src="/loading.svg" alt="loading indicator" width={256} height={256} className="absolute" />
<video ref={videoRef} className={`size-full z-10 ${setQrBytesRaw ? "object-cover" : ""}`} />
</div>
<video ref={videoRef} className={`size-full rounded-2xl border-2 border-amber-500 max-h-96 ${setQrBytesRaw ? "object-cover" : ""}`} />
{setQrBytesRaw && <QrFinder />}
<canvas ref={canvasRef} className="hidden" />
</div>

View file

@ -25,25 +25,6 @@ interface Props {
likes: number;
}
function deepMerge<T>(target: T, source: Partial<T>): T {
const output = structuredClone(target);
if (typeof source !== "object" || source === null) return output;
for (const key in source) {
const sourceValue = source[key];
const targetValue = (output as any)[key];
if (typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) {
(output as any)[key] = deepMerge(targetValue, sourceValue);
} else {
(output as any)[key] = sourceValue;
}
}
return output;
}
export default function EditForm({ mii, likes }: Props) {
const [files, setFiles] = useState<FileWithPath[]>([]);
@ -65,7 +46,7 @@ export default function EditForm({ mii, likes }: Props) {
const [makeup, setMakeup] = useState<MiiMakeup>(mii.makeup ?? "PARTIAL");
const hasFilesChanged = useRef(false);
const instructions = useRef<SwitchMiiInstructions>(deepMerge(defaultInstructions, (mii.instructions as object) ?? {}));
const instructions = useRef<SwitchMiiInstructions>({ ...defaultInstructions, ...(mii.instructions as object as Partial<SwitchMiiInstructions>) });
const handleSubmit = async () => {
// Validate before sending request

View file

@ -17,6 +17,7 @@ interface Props {
export default function SwitchFileUpload({ text, forceCrop, image, setImage }: Props) {
const [isCameraOpen, setIsCameraOpen] = useState(false);
const [isCropOpen, setIsCropOpen] = useState(false);
const [hasImage, setHasImage] = useState(false);
const handleDrop = useCallback(
(acceptedFiles: FileWithPath[]) => {
@ -25,6 +26,7 @@ export default function SwitchFileUpload({ text, forceCrop, image, setImage }: P
const reader = new FileReader();
reader.onload = async (event) => {
setImage(event.target!.result as string);
setHasImage(true);
if (forceCrop) setIsCropOpen(true);
};
reader.readAsDataURL(file);
@ -32,11 +34,16 @@ export default function SwitchFileUpload({ text, forceCrop, image, setImage }: P
[setImage],
);
useEffect(() => {
if (!isCameraOpen) return;
if (forceCrop) setIsCropOpen(true);
}, [isCameraOpen]);
return (
<div className="max-w-md w-full flex flex-col items-center gap-2">
<Dropzone onDrop={handleDrop} options={{ maxFiles: 1 }}>
<p className="text-center text-sm">
{!image ? (
{!hasImage ? (
<>
Drag and drop {text}
<br />
@ -59,14 +66,7 @@ export default function SwitchFileUpload({ text, forceCrop, image, setImage }: P
Crop Image
</button>
<Camera
isOpen={isCameraOpen}
setIsOpen={setIsCameraOpen}
setImage={setImage}
onCapture={() => {
if (forceCrop) setIsCropOpen(true);
}}
/>
<Camera isOpen={isCameraOpen} setIsOpen={setIsCameraOpen} setImage={setImage} />
<CropPortrait isOpen={isCropOpen} setIsOpen={setIsCropOpen} image={image} setImage={setImage} />
</div>
);