Files
MineGeo/components/CosmeticsShop.tsx
pavel444-byte c541cc09b4
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 7m10s
Add public profiles and cosmetic shop
2026-10-15 17:30:10 +05:00

153 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Check, Coins, Sparkles } from "lucide-react";
import { useI18n } from "@/components/I18nProvider";
import { PlayerAvatar } from "@/components/PixelArt";
import { api } from "@/lib/client-api";
import type {
CosmeticShopState,
ProfileCosmeticId,
} from "@/shared/profile-cosmetics";
export function CosmeticsShop({
accountId,
avatarPath,
initialShop,
}: {
accountId: string;
avatarPath?: string | null;
initialShop: CosmeticShopState;
}) {
const { t, formatNumber } = useI18n();
const router = useRouter();
const [shop, setShop] = useState(initialShop);
const [busy, setBusy] = useState<string | null>(null);
const [error, setError] = useState("");
const [message, setMessage] = useState("");
async function change(action: "purchase" | "equip", cosmeticId: ProfileCosmeticId | null) {
const requestId = cosmeticId ?? "none";
setBusy(requestId);
setError("");
setMessage("");
try {
const result = await api<{ shop: CosmeticShopState }>("cosmetics", {
action,
cosmeticId,
});
setShop(result.shop);
setMessage(
action === "purchase"
? "Cosmetic purchased and equipped."
: cosmeticId
? "Cosmetic equipped."
: "Profile effect removed.",
);
router.refresh();
} catch (reason) {
setError((reason as Error).message);
} finally {
setBusy(null);
}
}
return (
<div className="cosmetics-shop">
<div className="cosmetics-shop-heading">
<div>
<span className="eyebrow">{t("THE EXPLORER’S WARDROBE")}</span>
<h1>{t("Leave a little sparkle.")}</h1>
<p className="lead">
{t("Earn GeoCoins in solo expeditions and dress up your profile portrait.")}
</p>
</div>
<div className="cosmetics-wallet" aria-label={t("GeoCoins balance")}>
<Coins size={20} aria-hidden="true" />
<span>
<small>{t("YOUR BALANCE")}</small>
<strong>{formatNumber(shop.balance)} {t("GeoCoins")}</strong>
</span>
</div>
</div>
<p className="cosmetics-earning-note">
<Sparkles size={17} aria-hidden="true" />
{t("Complete a public solo game to earn 10–20 GeoCoins. Rewards are capped at 100 every 24 hours.")}
</p>
{error && <p className="error-message" role="alert">{t(error)}</p>}
{message && <p className="success-message" role="status">{t(message)}</p>}
<div className="cosmetics-grid">
{shop.catalog.map((cosmetic) => {
const owned = shop.owned.includes(cosmetic.id);
const equipped = shop.equipped === cosmetic.id;
const itemBusy = busy === cosmetic.id;
return (
<article
className="cosmetic-card"
data-rarity={cosmetic.rarity}
key={cosmetic.id}
>
<div className="cosmetic-card-top">
<span className="cosmetic-rarity">{t(cosmetic.rarity)}</span>
{equipped && (
<span className="cosmetic-equipped">
<Check size={13} aria-hidden="true" /> {t("Equipped")}
</span>
)}
</div>
<div className="cosmetic-preview">
<PlayerAvatar
id={accountId}
avatarPath={avatarPath}
effect={cosmetic.id}
className="cosmetic-preview-avatar"
/>
</div>
<h2>{t(cosmetic.name)}</h2>
<p>{t(cosmetic.description)}</p>
<div className="cosmetic-card-action">
{owned ? (
<button
className={equipped ? "button" : "button primary"}
type="button"
onClick={() =>
change("equip", equipped ? null : cosmetic.id)
}
disabled={busy !== null}
>
{itemBusy
? t("Saving…")
: equipped
? t("Unequip effect")
: t("Equip effect")}
</button>
) : (
<button
className="button primary"
type="button"
onClick={() => change("purchase", cosmetic.id)}
disabled={busy !== null || shop.balance < cosmetic.price}
>
{itemBusy
? t("Buying…")
: t("Buy · {price} GeoCoins", {
price: formatNumber(cosmetic.price),
})}
</button>
)}
</div>
</article>
);
})}
</div>
<p className="cosmetics-real-money-note">
{t("Cosmetics use GeoCoins for now. Payment options may be added later.")}
</p>
</div>
);
}