Initial commit
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import {
|
||||
priceCollectionCards,
|
||||
type MyCardRecord,
|
||||
} from "../../lib/collection";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const { username } = Astro.params;
|
||||
|
||||
if (!username) {
|
||||
return Astro.redirect("/friends");
|
||||
}
|
||||
|
||||
// Find the target user by username (same public view the friends API uses)
|
||||
let targetUser;
|
||||
try {
|
||||
targetUser = await pb
|
||||
.collection("public_users")
|
||||
.getFirstListItem(pb.filter("username = {:username}", { username }));
|
||||
} catch {
|
||||
return Astro.redirect("/friends");
|
||||
}
|
||||
|
||||
// Check visibility — Private users' collections are never visible
|
||||
if (targetUser.visibility === "Private") {
|
||||
return Astro.redirect("/friends");
|
||||
}
|
||||
|
||||
// Friends-only collections require an accepted friendship
|
||||
if (targetUser.visibility !== "Public") {
|
||||
const userId = pb.authStore.record!.id;
|
||||
const friendship = await pb.collection("friendships").getList(1, 1, {
|
||||
filter: pb.filter(
|
||||
"((requester = {:userId} && addressee = {:targetId}) || (requester = {:targetId} && addressee = {:userId})) && status = 'accepted'",
|
||||
{ userId, targetId: targetUser.id },
|
||||
),
|
||||
});
|
||||
|
||||
if (friendship.items.length === 0) {
|
||||
return Astro.redirect("/friends");
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch their collection
|
||||
const myCards = (await pb.collection("mycards").getFullList({
|
||||
filter: pb.filter("user = {:userId}", { userId: targetUser.id }),
|
||||
sort: "-created",
|
||||
})) as unknown as MyCardRecord[];
|
||||
|
||||
const cardsWithPrice = await priceCollectionCards(myCards);
|
||||
|
||||
const totalValue = cardsWithPrice.reduce((s, c) => s + c.totalPrice, 0);
|
||||
const totalCards = cardsWithPrice.reduce((s, c) => s + c.quantity, 0);
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-[1600px] mx-auto px-4 md:px-8 pb-12 mt-8">
|
||||
<a
|
||||
href="/friends"
|
||||
class="inline-block no-underline opacity-70 hover:opacity-100 text-sm mb-4"
|
||||
>← Amis</a
|
||||
>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="avatar" data-size="lg">
|
||||
<span
|
||||
>{
|
||||
(
|
||||
targetUser.name ||
|
||||
targetUser.username ||
|
||||
targetUser.email ||
|
||||
"??"
|
||||
)
|
||||
.slice(0, 2)
|
||||
.toUpperCase()
|
||||
}</span
|
||||
>
|
||||
</span>
|
||||
<div>
|
||||
<h1 class="m-0">{targetUser.name || "—"}</h1>
|
||||
<p class="text-sm opacity-60 m-0">
|
||||
@{targetUser.username || "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-6">
|
||||
<div
|
||||
class="bg-muted rounded-lg px-6 py-3 flex flex-col items-center"
|
||||
>
|
||||
<span class="text-xs uppercase tracking-wide opacity-70"
|
||||
>Cartes</span
|
||||
>
|
||||
<span class="text-2xl font-bold">{totalCards}</span>
|
||||
</div>
|
||||
<div
|
||||
class="bg-muted rounded-lg px-6 py-3 flex flex-col items-center"
|
||||
>
|
||||
<span class="text-xs uppercase tracking-wide opacity-70"
|
||||
>Valeur totale</span
|
||||
>
|
||||
<span class="text-2xl font-bold"
|
||||
>{totalValue.toFixed(2)} €</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
cardsWithPrice.length === 0 ? (
|
||||
<section class="empty">
|
||||
<header>
|
||||
<h3>Collection vide</h3>
|
||||
<p>
|
||||
{targetUser.name || targetUser.username} n'a pas
|
||||
encore de cartes dans sa collection.
|
||||
</p>
|
||||
</header>
|
||||
</section>
|
||||
) : (
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{cardsWithPrice.map((card) => (
|
||||
<a
|
||||
href={`/card/${card.card_id}`}
|
||||
class="card no-underline hover:-translate-y-0.5 hover:shadow-lg transition-all duration-150 overflow-hidden"
|
||||
>
|
||||
<div class="aspect-2.5/3.5 overflow-hidden">
|
||||
{card.card_image ? (
|
||||
<img
|
||||
src={`${card.card_image}/low.webp`}
|
||||
alt={card.card_name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/assets/placeholder_card.png"
|
||||
alt={card.card_name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div class="p-3 flex flex-col gap-1.5">
|
||||
<h3 class="m-0 text-sm font-semibold leading-tight truncate">
|
||||
{card.card_name}
|
||||
</h3>
|
||||
{card.set_name && (
|
||||
<small class="opacity-60 text-xs truncate">
|
||||
{card.set_name}
|
||||
</small>
|
||||
)}
|
||||
<div class="flex items-center justify-between">
|
||||
<span
|
||||
class="badge"
|
||||
data-variant="secondary"
|
||||
>
|
||||
{card.variant}
|
||||
</span>
|
||||
<span class="font-semibold text-sm">
|
||||
×{card.quantity}
|
||||
</span>
|
||||
</div>
|
||||
{card.condition && (
|
||||
<span
|
||||
class="badge"
|
||||
data-variant={
|
||||
card.condition === "Neuf"
|
||||
? "primary"
|
||||
: card.condition === "Joué"
|
||||
? "destructive"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{card.condition}
|
||||
</span>
|
||||
)}
|
||||
<div class="flex items-center justify-between mt-2 pt-2 border-t border-border">
|
||||
<span class="font-bold text-base">
|
||||
{card.price.toFixed(2)} €
|
||||
</span>
|
||||
{card.quantity > 1 && (
|
||||
<span class="text-xs opacity-70">
|
||||
= {card.totalPrice.toFixed(2)} €
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style is:global>
|
||||
a.card {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user