feat(me): organize personal dashboard and localize user actions
This commit is contained in:
1 parent
2840ef5d9d
commit
85a6df162b
9 files changed
+717
-525
No files matched your search
@@ -1,11 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function CopyReferralButton({ value }: { value: string }) {
|
export default function CopyReferralButton({ value }: { value: string }) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
const t = useTranslations("pages.myDashboard");
|
||||||
|
|
||||||
async function copy() {
|
async function copy() {
|
||||||
|
setFailed(false);
|
||||||
|
setCopied(false);
|
||||||
const absolute =
|
const absolute =
|
||||||
typeof window !== "undefined" && value.startsWith("/")
|
typeof window !== "undefined" && value.startsWith("/")
|
||||||
? `${window.location.origin}${value}`
|
? `${window.location.origin}${value}`
|
||||||
@@ -20,9 +25,11 @@ export default function CopyReferralButton({ value }: { value: string }) {
|
|||||||
document.body.appendChild(ta);
|
document.body.appendChild(ta);
|
||||||
ta.select();
|
ta.select();
|
||||||
try {
|
try {
|
||||||
document.execCommand("copy");
|
if (!document.execCommand("copy")) throw new Error("copy-failed");
|
||||||
} catch {
|
} catch {
|
||||||
/* give up silently */
|
setFailed(true);
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
document.body.removeChild(ta);
|
document.body.removeChild(ta);
|
||||||
}
|
}
|
||||||
@@ -31,17 +38,21 @@ export default function CopyReferralButton({ value }: { value: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={copy}
|
onClick={copy}
|
||||||
className="rounded-xl font-bold text-sm px-5 py-2.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shrink-0"
|
className="rounded-xl font-bold text-sm px-5 py-2.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shrink-0"
|
||||||
style={{
|
style={{
|
||||||
background: "color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
background:
|
||||||
|
"color-mix(in srgb, var(--color-primary) 12%, transparent)",
|
||||||
color: "var(--color-primary-readable, var(--color-primary))",
|
color: "var(--color-primary-readable, var(--color-primary))",
|
||||||
border: `2px solid color-mix(in srgb, var(--color-primary) 20%, transparent)`,
|
border: `2px solid color-mix(in srgb, var(--color-primary) 20%, transparent)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{copied ? "Copied!" : "Copy link"}
|
{copied ? t("copied") : t("copy")}
|
||||||
</button>
|
</button>
|
||||||
|
{failed && <p role="alert">{t("copyFailed")}</p>}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+265
-518
File diff suppressed because it is too large.
Load diff
@@ -8,6 +8,7 @@ import { sendFriendRequest } from "@/actions/social";
|
|||||||
import Link from "@/components/link";
|
import Link from "@/components/link";
|
||||||
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
|
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
|
||||||
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
||||||
|
import { ProfileImage } from "@/components/shared/profile-image";
|
||||||
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +26,6 @@ import {
|
|||||||
import { avatarImageUrl } from "@/lib/format";
|
import { avatarImageUrl } from "@/lib/format";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import styles from "./profile.module.css";
|
import styles from "./profile.module.css";
|
||||||
import { ProfileImage } from "./profile-image";
|
|
||||||
|
|
||||||
type SearchParams = Promise<{
|
type SearchParams = Promise<{
|
||||||
friend?: string;
|
friend?: string;
|
||||||
|
|||||||
File renamed without changes.
@@ -0,0 +1,53 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { createTranslator } from "next-intl";
|
||||||
|
import { expect, it } from "vitest";
|
||||||
|
import en from "@/messages/en.json";
|
||||||
|
import italian from "@/messages/it.json";
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["en", en],
|
||||||
|
["it", italian],
|
||||||
|
] as const)("renders all dashboard messages in %s", (locale, messages) => {
|
||||||
|
const errors: unknown[] = [];
|
||||||
|
const t = createTranslator({
|
||||||
|
locale,
|
||||||
|
messages: messages as typeof en,
|
||||||
|
namespace: "pages.myDashboard",
|
||||||
|
onError: (error) => errors.push(error),
|
||||||
|
});
|
||||||
|
const keys = new Set(
|
||||||
|
["credits", "duckets", "diamonds", "points"].map(
|
||||||
|
(key) => `currencies.${key}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (const error of [
|
||||||
|
"not_enough",
|
||||||
|
"no_referrals",
|
||||||
|
"bad_config",
|
||||||
|
"ratelimit",
|
||||||
|
"error",
|
||||||
|
])
|
||||||
|
keys.add(`errors.${error}`);
|
||||||
|
for (const file of ["page.tsx", "CopyReferralButton.tsx"]) {
|
||||||
|
for (const match of readFileSync(
|
||||||
|
`src/app/(site)/me/${file}`,
|
||||||
|
"utf8",
|
||||||
|
).matchAll(/\bt\("([^"]+)"/g))
|
||||||
|
keys.add(match[1]);
|
||||||
|
}
|
||||||
|
for (const key of keys) {
|
||||||
|
const message = key as Parameters<typeof t>[0];
|
||||||
|
expect(t.has(message), key).toBe(true);
|
||||||
|
expect(
|
||||||
|
t(message, {
|
||||||
|
count: 3,
|
||||||
|
amount: "30",
|
||||||
|
currency: "Diamanti",
|
||||||
|
hotel: "Epicnabbo",
|
||||||
|
date: "06/09/2026",
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
).not.toBe("");
|
||||||
|
}
|
||||||
|
expect(errors).toEqual([]);
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
import { and, asc, count, desc, eq, gt, or } from "drizzle-orm";
|
import { and, asc, count, desc, eq, gt, inArray, or } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
MessengerFriendships,
|
MessengerFriendships,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
User,
|
User,
|
||||||
UserReferrals,
|
UserReferrals,
|
||||||
UsersBadges,
|
UsersBadges,
|
||||||
|
UsersCurrency,
|
||||||
UsersSettings,
|
UsersSettings,
|
||||||
} from "@/lib/db";
|
} from "@/lib/db";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
@@ -27,6 +28,7 @@ export async function loadUserDashboard(userId: number) {
|
|||||||
unreadCountRows,
|
unreadCountRows,
|
||||||
referralsRows,
|
referralsRows,
|
||||||
friends,
|
friends,
|
||||||
|
currencyRows,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select({
|
.select({
|
||||||
@@ -34,7 +36,6 @@ export async function loadUserDashboard(userId: number) {
|
|||||||
username: User.username,
|
username: User.username,
|
||||||
look: User.look,
|
look: User.look,
|
||||||
motto: User.motto,
|
motto: User.motto,
|
||||||
rank: User.rank,
|
|
||||||
credits: User.credits,
|
credits: User.credits,
|
||||||
accountCreated: User.accountCreated,
|
accountCreated: User.accountCreated,
|
||||||
lastLogin: User.lastLogin,
|
lastLogin: User.lastLogin,
|
||||||
@@ -132,6 +133,16 @@ export async function loadUserDashboard(userId: number) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
|
db
|
||||||
|
.select({ type: UsersCurrency.type, amount: UsersCurrency.amount })
|
||||||
|
.from(UsersCurrency)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(UsersCurrency.userId, userId),
|
||||||
|
inArray(UsersCurrency.type, [0, 5]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -148,5 +159,6 @@ export async function loadUserDashboard(userId: number) {
|
|||||||
unreadCountRows,
|
unreadCountRows,
|
||||||
referralsRows,
|
referralsRows,
|
||||||
friends,
|
friends,
|
||||||
|
currencyRows,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3475,6 +3475,66 @@
|
|||||||
"description": "Try again later or contact an administrator."
|
"description": "Try again later or contact an administrator."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"myDashboard": {
|
||||||
|
"title": "My dashboard",
|
||||||
|
"loadError": "We could not load your dashboard. Please try again.",
|
||||||
|
"retry": "Try again",
|
||||||
|
"login": "Sign in",
|
||||||
|
"never": "Never",
|
||||||
|
"claimed": "Reward claimed successfully!",
|
||||||
|
"welcome": "Your space on {hotel}",
|
||||||
|
"noMotto": "No motto yet",
|
||||||
|
"registered": "Registered: {date}",
|
||||||
|
"lastOnline": "Last online: {date}",
|
||||||
|
"enter": "Enter {hotel}",
|
||||||
|
"quickLinks": "Quick links",
|
||||||
|
"messages": "Messages",
|
||||||
|
"profile": "My profile",
|
||||||
|
"settings": "Settings",
|
||||||
|
"shop": "Shop",
|
||||||
|
"unread": "{count} unread messages",
|
||||||
|
"wallet": "Your wallet",
|
||||||
|
"currencies": {
|
||||||
|
"credits": "Credits",
|
||||||
|
"duckets": "Duckets",
|
||||||
|
"diamonds": "Diamonds",
|
||||||
|
"points": "Points"
|
||||||
|
},
|
||||||
|
"onlineFriends": "Online friends",
|
||||||
|
"allFriends": "All friends",
|
||||||
|
"noOnlineFriends": "None of your friends are online right now.",
|
||||||
|
"recentRooms": "New rooms",
|
||||||
|
"community": "Community",
|
||||||
|
"roomsHint": "The newest rooms created in the hotel.",
|
||||||
|
"noRooms": "No rooms available yet.",
|
||||||
|
"activity": "Your activity",
|
||||||
|
"achievements": "Achievement score",
|
||||||
|
"respects": "Respects received",
|
||||||
|
"friends": "Friends",
|
||||||
|
"lastLogin": "Last login",
|
||||||
|
"referrals": "Invite your friends",
|
||||||
|
"referralHint": "Share your link and earn rewards when new players register.",
|
||||||
|
"referralProgress": "Progress towards your reward",
|
||||||
|
"reward": "{amount} {currency} for every {count} new players.",
|
||||||
|
"rewardHint": "Invite {count} players to unlock your reward.",
|
||||||
|
"referralLink": "Your invitation link",
|
||||||
|
"claim": "Claim reward",
|
||||||
|
"remaining": "{count, plural, one {Invite # more player to unlock your reward.} other {Invite # more players to unlock your reward.}}",
|
||||||
|
"rules": "Invitation rules",
|
||||||
|
"rulesText": "Creating extra accounts to increase invitations can lead to removal of progress, currency and inventory, and a ban.",
|
||||||
|
"badges": "Equipped badges",
|
||||||
|
"noBadges": "You are not wearing any badges yet.",
|
||||||
|
"copy": "Copy link",
|
||||||
|
"copied": "Copied!",
|
||||||
|
"copyFailed": "Copy failed. Select the link and copy it manually.",
|
||||||
|
"errors": {
|
||||||
|
"not_enough": "You need more invitations to claim the reward.",
|
||||||
|
"no_referrals": "Share your link to invite your first friend.",
|
||||||
|
"bad_config": "The reward is not configured correctly. Contact the staff.",
|
||||||
|
"ratelimit": "Too many attempts. Wait a moment and try again.",
|
||||||
|
"error": "The reward could not be claimed. Please try again."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
|
|||||||
@@ -3442,6 +3442,66 @@
|
|||||||
"description": "Riprova più tardi o contatta un amministratore."
|
"description": "Riprova più tardi o contatta un amministratore."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"myDashboard": {
|
||||||
|
"title": "La mia pagina",
|
||||||
|
"loadError": "Impossibile caricare la tua pagina. Riprova.",
|
||||||
|
"retry": "Riprova",
|
||||||
|
"login": "Accedi",
|
||||||
|
"never": "Mai",
|
||||||
|
"claimed": "Premio riscattato!",
|
||||||
|
"welcome": "Il tuo spazio su {hotel}",
|
||||||
|
"noMotto": "Nessun motto",
|
||||||
|
"registered": "Iscritto dal {date}",
|
||||||
|
"lastOnline": "Ultima presenza: {date}",
|
||||||
|
"enter": "Entra in {hotel}",
|
||||||
|
"quickLinks": "Collegamenti rapidi",
|
||||||
|
"messages": "Messaggi",
|
||||||
|
"profile": "Il mio profilo",
|
||||||
|
"settings": "Impostazioni",
|
||||||
|
"shop": "Negozio",
|
||||||
|
"unread": "{count} messaggi non letti",
|
||||||
|
"wallet": "Il tuo portafoglio",
|
||||||
|
"currencies": {
|
||||||
|
"credits": "Crediti",
|
||||||
|
"duckets": "Duckets",
|
||||||
|
"diamonds": "Diamanti",
|
||||||
|
"points": "Punti"
|
||||||
|
},
|
||||||
|
"onlineFriends": "Amici online",
|
||||||
|
"allFriends": "Tutti gli amici",
|
||||||
|
"noOnlineFriends": "Nessuno dei tuoi amici è online al momento.",
|
||||||
|
"recentRooms": "Nuove stanze",
|
||||||
|
"community": "Community",
|
||||||
|
"roomsHint": "Le ultime stanze create nell’hotel.",
|
||||||
|
"noRooms": "Nessuna stanza disponibile.",
|
||||||
|
"activity": "La tua attività",
|
||||||
|
"achievements": "Punti traguardo",
|
||||||
|
"respects": "Rispetti ricevuti",
|
||||||
|
"friends": "Amici",
|
||||||
|
"lastLogin": "Ultimo accesso",
|
||||||
|
"referrals": "Invita i tuoi amici",
|
||||||
|
"referralHint": "Condividi il tuo link e ricevi premi quando si registrano nuovi giocatori.",
|
||||||
|
"referralProgress": "Progressi verso il premio",
|
||||||
|
"reward": "{amount} {currency} ogni {count} nuovi giocatori.",
|
||||||
|
"rewardHint": "Invita {count} giocatori per sbloccare il premio.",
|
||||||
|
"referralLink": "Il tuo link di invito",
|
||||||
|
"claim": "Riscatta premio",
|
||||||
|
"remaining": "{count, plural, one {Invita ancora # giocatore per sbloccare il premio.} other {Invita ancora # giocatori per sbloccare il premio.}}",
|
||||||
|
"rules": "Regole degli inviti",
|
||||||
|
"rulesText": "Creare altri account per aumentare gli inviti può comportare la rimozione di progressi, valute e inventario e il ban.",
|
||||||
|
"badges": "Distintivi indossati",
|
||||||
|
"noBadges": "Non stai ancora indossando distintivi.",
|
||||||
|
"copy": "Copia link",
|
||||||
|
"copied": "Copiato!",
|
||||||
|
"copyFailed": "Copia non riuscita. Seleziona il link e copialo manualmente.",
|
||||||
|
"errors": {
|
||||||
|
"not_enough": "Servono altri inviti per riscattare il premio.",
|
||||||
|
"no_referrals": "Condividi il link per invitare il tuo primo amico.",
|
||||||
|
"bad_config": "Il premio non è configurato correttamente. Contatta lo staff.",
|
||||||
|
"ratelimit": "Troppi tentativi. Attendi un momento e riprova.",
|
||||||
|
"error": "Non è stato possibile riscattare il premio. Riprova."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
|
|||||||
Reference in new issue
Block a user