Compare commits

..

12 commits

Author SHA1 Message Date
7bd84ea454
fix: auth broken? 2026-04-19 23:38:10 +01:00
18a49a74e8
Merge pull request #41 from Y4ELX/fix-tag-links-on-mii-details-to-use-query-parameters
fix: update tag links to use query parameters
2026-04-19 22:21:00 +01:00
Yael Monterrubio
6211adc548 fix: update tag links to use query parameters 2026-04-19 15:17:19 -06:00
61cbdad812 fix: profile picture points to mii pictures
i copy pasted the wrong thing
2026-04-19 22:01:24 +01:00
7f87a42b11
Merge pull request #39 from Y4ELX/fix-infinite-loading-missing-resources
fix: handle not found errors for Mii and User in API responses
2026-04-19 21:58:14 +01:00
Yael Monterrubio
d5b488a5c9 fix: handle not found errors for Mii and User in API responses 2026-04-19 14:54:15 -06:00
8c12a835a6
Merge pull request #38 from Y4ELX/fix-profile-own-detection
fix: refactor user role checks in ProfileLayout
2026-04-19 21:48:55 +01:00
Yael Monterrubio
2bbc6d7c05 fix: refactor user role checks in ProfileLayout 2026-04-19 14:43:17 -06:00
90a65102c1 revert c951c7d7 2026-04-19 21:26:09 +01:00
d58054a587 fix: site broke 2026-04-19 21:16:48 +01:00
583d223ed9 fix: better like count 2026-04-19 18:16:10 +01:00
c951c7d755 feat: move images to nginx 2026-04-19 17:52:15 +01:00
15 changed files with 46 additions and 31 deletions

View file

@ -5,7 +5,7 @@ REDIS_URL="redis://localhost:6379/0"
# Used for metadata, sitemaps, etc.
NEXT_PUBLIC_BASE_URL=http://localhost:3000
FRONTEND_URL=http://localhost:4321
NEXT_PUBLIC_FRONTEND_URL=http://localhost:5173
CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX
CLOUDFLARE_API_TOKEN=XXXXXXXXXXXXXXXX

View file

@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "miis_in_queue_quarantined_createdAt_idx" ON "miis"("in_queue", "quarantined", "createdAt" DESC);

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "miis" ADD COLUMN "likeCount" INTEGER NOT NULL DEFAULT 0;
-- CreateIndex
CREATE INDEX "miis_likeCount_idx" ON "miis"("likeCount" DESC);

View file

@ -91,19 +91,22 @@ model Mii {
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
likedBy Like[]
likeCount Int @default(0)
punishmentId Int?
punishments MiiPunishment[]
likedBy Like[]
@@index([tags], type: Gin)
@@index([createdAt])
@@index([likeCount(sort: Desc)])
@@index([quarantined, createdAt(sort: Desc)])
@@index([platform, createdAt(sort: Desc)])
@@index([userId, createdAt(sort: Desc)])
@@index([gender])
@@index([makeup])
@@index([quarantined, id])
@@index([in_queue, quarantined, createdAt(sort: Desc)])
@@map("miis")
}

View file

@ -34,5 +34,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
},
});
if (!mii) return NextResponse.json({ error: "Mii not found" }, { status: 404 });
return NextResponse.json(mii);
}

View file

@ -29,7 +29,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
});
if (existingLike) {
// Remove the like if it exists
await tx.like.delete({
where: {
userId_miiId: {
@ -38,22 +37,25 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
},
},
});
await tx.mii.update({
where: { id: miiId },
data: { likeCount: { decrement: 1 } },
});
} else {
// Add a like if it doesn't exist
await tx.like.create({
data: {
userId: Number(session.user?.id),
miiId,
},
});
await tx.mii.update({
where: { id: miiId },
data: { likeCount: { increment: 1 } },
});
}
const likeCount = await tx.like.count({
where: { miiId },
return { liked: !existingLike };
});
return { liked: !existingLike, count: likeCount };
});
return rateLimit.sendResponse({ success: true, liked: result.liked, count: result.count });
return rateLimit.sendResponse({ success: true, liked: result.liked });
}

View file

@ -78,6 +78,7 @@ export async function GET(request: NextRequest) {
allowedCopying: true,
quarantined: true,
in_queue: true,
likeCount: true,
// Mii liked check
...(session?.user?.id && {
likedBy: {
@ -85,10 +86,6 @@ export async function GET(request: NextRequest) {
select: { userId: true },
},
}),
// Like count
_count: {
select: { likedBy: true },
},
// Admin
...(parentPage === "admin" && {
description: true,
@ -102,7 +99,7 @@ export async function GET(request: NextRequest) {
let orderBy: Prisma.MiiOrderByWithRelationInput[];
if (sort === "likes") {
orderBy = [{ likedBy: { _count: "desc" } }, { name: "asc" }];
orderBy = [{ likeCount: "desc" }, { name: "asc" }];
} else if (sort === "oldest") {
orderBy = [{ createdAt: "asc" }, { name: "asc" }];
} else {

View file

@ -21,5 +21,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
},
});
if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 });
return NextResponse.json(user);
}

View file

@ -44,6 +44,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
},
async redirect({ url, baseUrl }) {
if (url.startsWith(baseUrl)) return url;
return process.env.NEXT_PUBLIC_FRONTEND_URL ?? "http://localhost:4321";
},
},

View file

@ -30,7 +30,6 @@ export default function LikeButton({ likes, miiId, isLiked, disabled, abbreviate
}
const prevLiked = isLikedState;
const prevLikes = likesState;
setIsLikedState(!prevLiked);
setLikesState(prevLiked ? likesState - 1 : likesState + 1);
@ -42,12 +41,10 @@ export default function LikeButton({ likes, miiId, isLiked, disabled, abbreviate
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/mii/${miiId}/like`, { method: "POST", credentials: "include" });
if (response.ok) {
const { liked, count } = await response.json();
const { liked } = await response.json();
setIsLikedState(liked);
setLikesState(count);
} else {
setIsLikedState(prevLiked);
setLikesState(prevLikes);
}
};

View file

@ -20,7 +20,7 @@ export default function AuthorButtons({ mii }: Props) {
<Icon icon="mdi:pencil" />
<span>Edit</span>
</Link>
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy ?? 0} inMiiPage />
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount ?? 0} inMiiPage />
</>
);
}

View file

@ -172,7 +172,7 @@ export default function MiiList({ parentPage, userId }: Props) {
{parentPage === "admin" && mii.description && <Description text={mii.description} />}
<div className="mt-auto grid grid-cols-2 items-center">
<LikeButton likes={mii._count.likedBy} miiId={mii.id} isLiked={likedIds.has(mii.id)} abbreviate />
<LikeButton likes={mii.likeCount} miiId={mii.id} isLiked={likedIds.has(mii.id)} abbreviate />
{!userId && (
<Link to={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
@ -185,7 +185,7 @@ export default function MiiList({ parentPage, userId }: Props) {
<Link to={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit">
<Icon icon="mdi:pencil" />
</Link>
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount} />
</div>
)}
@ -202,7 +202,7 @@ export default function MiiList({ parentPage, userId }: Props) {
<Icon icon="material-symbols:check-rounded" />
</button>
<div className="text-zinc-400 hover:text-red-500 transition-colors p-1 bg-white rounded-md shadow-sm border border-zinc-200 hover:border-red-500 flex items-center justify-center">
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount} />
</div>
</div>

View file

@ -28,7 +28,7 @@ export default function ShareMiiButton({ miiId }: Props) {
};
const handleCopyImage = async () => {
const response = await fetch(`${import.meta.env.VITE_API_URL}/mii/${miiId}/image?type=metadata`);
const response = await fetch(`${import.meta.env.VITE_BASE_URL}/mii/${miiId}/image?type=metadata`);
const blob = await response.blob();
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);

View file

@ -31,6 +31,8 @@ export default function MiiPage() {
return res.json();
})
.then((data) => {
if (!data) throw new Error("Mii not found");
setMii(data);
setLoading(false);
@ -264,12 +266,12 @@ export default function MiiPage() {
{/* Submission name */}
<h1 className="text-4xl font-extrabold wrap-break-word whitespace-break-spaces text-amber-700 flex-1 min-w-0">{mii.name}</h1>
{/* Like button */}
<LikeButton likes={mii._count?.likedBy ?? 0} miiId={mii.id} isLiked={isLiked} big />
<LikeButton likes={mii.likeCount ?? 0} miiId={mii.id} isLiked={isLiked} 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">
{mii.tags.map((tag: string) => (
<Link to={`/tags=${tag}`} key={tag}>
<Link to={`/?tags=${encodeURIComponent(tag)}`} key={tag}>
{tag}
</Link>
))}

View file

@ -28,6 +28,8 @@ export default function ProfileLayout() {
return res.json();
})
.then((data) => {
if (!data) throw new Error("Profile not found");
setUser(data);
setLoading(false);
})
@ -42,11 +44,11 @@ export default function ProfileLayout() {
return <div className="p-6 text-center">Loading...</div>;
}
const currentUser = user ?? $session?.user;
const sessionUserId = $session?.user?.id ? Number($session.user.id) : null;
const page = location.pathname;
const isAdmin = currentUser?.id === Number(import.meta.env.VITE_ADMIN_USER_ID);
const isContributor = import.meta.env.VITE_CONTRIBUTORS_USER_IDS?.split(",").includes(user?.id);
const isOwnProfile = currentUser?.id === user?.id;
const isAdmin = sessionUserId === Number(import.meta.env.VITE_ADMIN_USER_ID);
const isContributor = import.meta.env.VITE_CONTRIBUTORS_USER_IDS?.split(",").includes(String(user?.id));
const isOwnProfile = sessionUserId === user?.id;
return (
<div>