Add public profiles and cosmetic shop
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 7m10s

This commit is contained in:
2026-10-15 17:30:10 +05:00
parent 44e62cff2f
commit c541cc09b4
28 changed files with 1359 additions and 45 deletions

View File

@@ -43,13 +43,26 @@ export const profileUpdateSchema = z
displayName: displayNameSchema.optional(),
glowColor: glowColorSchema.optional(),
avatarPath: z.string().max(128).nullable().optional(),
bio: z
.string()
.trim()
.max(280, "Use at most 280 characters")
.refine(
(value) =>
!/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069<>]/u.test(
value,
),
"Use plain text without hidden controls or markup",
)
.optional(),
})
.strict()
.refine(
(value) =>
value.displayName !== undefined ||
value.glowColor !== undefined ||
value.avatarPath !== undefined,
value.avatarPath !== undefined ||
value.bio !== undefined,
"Choose a profile change",
);
export const initialPasswordSchema = z

View File

@@ -568,6 +568,8 @@ export type Database = {
profiles: {
Row: AccountStatusFields & {
avatar_path: string | null;
bio: string;
profile_effect: string | null;
glow_color: string;
bug_finder: boolean;
custom_prefix: string | null;
@@ -580,6 +582,8 @@ export type Database = {
};
Insert: Partial<AccountStatusFields> & {
avatar_path?: string | null;
bio?: string;
profile_effect?: string | null;
glow_color?: string;
bug_finder?: boolean;
custom_prefix?: string | null;
@@ -592,6 +596,8 @@ export type Database = {
};
Update: Partial<AccountStatusFields> & {
avatar_path?: string | null;
bio?: string;
profile_effect?: string | null;
glow_color?: string;
bug_finder?: boolean;
custom_prefix?: string | null;
@@ -782,6 +788,18 @@ export type Database = {
[_ in never]: never;
};
Functions: {
cosmetics_shop_state: {
Args: { p_user: string };
Returns: Json;
};
buy_profile_cosmetic: {
Args: { p_user: string; p_cosmetic_id: string };
Returns: Json;
};
equip_profile_cosmetic: {
Args: { p_user: string; p_cosmetic_id: string | null };
Returns: Json;
};
admin_account_action: {
Args: {
p_reviewer: string;

View File

@@ -10,6 +10,55 @@
"Guide": "Как играть",
"Docs": "Документация",
"My account": "Мой аккаунт",
"Profile cosmetics": "Косметика профиля",
"Browse profile cosmetics": "Открыть магазин косметики",
"View public profile": "Открыть публичный профиль",
"About you": "О себе",
"Tell other explorers a little about yourself.": "Расскажите другим исследователям немного о себе.",
"Shown on your public profile · Maximum 280 characters.": "Отображается в публичном профиле · Не более 280 символов.",
"Save public profile": "Сохранить публичный профиль",
"Public profile saved.": "Публичный профиль сохранён.",
"A name and description for other explorers to see.": "Имя и описание, которые увидят другие исследователи.",
"Back to leaderboard": "Назад к рейтингу",
"MINEGEO EXPLORER": "ИССЛЕДОВАТЕЛЬ MINEGEO",
"Exploring since {date}": "Исследует миры с {date}",
"About this explorer": "Об исследователе",
"This explorer hasn’t added a description yet.": "Исследователь пока не добавил описание.",
"Profile stats": "Статистика профиля",
"Completed journeys": "Завершённые экспедиции",
"Published maps": "Опубликованные карты",
"Only public account details and published maps appear here.": "Здесь отображаются только публичные данные аккаунта и опубликованные карты.",
"THE EXPLORER’S WARDROBE": "ГАРДЕРОБ ИССЛЕДОВАТЕЛЯ",
"Leave a little sparkle.": "Добавь немного сияния.",
"Earn GeoCoins in solo expeditions and dress up your profile portrait.": "Зарабатывай GeoCoins в одиночных экспедициях и укрась портрет профиля.",
"GeoCoins balance": "Баланс GeoCoins",
"YOUR BALANCE": "ТВОЙ БАЛАНС",
"GeoCoins": "GeoCoins",
"Complete a public solo game to earn 10–20 GeoCoins. Rewards are capped at 100 every 24 hours.": "Заверши одиночную игру на публичной карте и получи 10–20 GeoCoins. Максимум — 100 за каждые 24 часа.",
"common": "обычный",
"rare": "редкий",
"epic": "эпический",
"legendary": "легендарный",
"Equipped": "Надето",
"Cosmetic purchased and equipped.": "Эффект куплен и надет.",
"Cosmetic equipped.": "Эффект надет.",
"Profile effect removed.": "Эффект профиля снят.",
"Not enough GeoCoins": "Недостаточно GeoCoins.",
"Cosmetic is not available": "Эта косметика сейчас недоступна.",
"Cosmetic not owned": "Сначала купи этот эффект.",
"Buy · {price} GeoCoins": "Купить · {price} GeoCoins",
"Unequip effect": "Снять эффект",
"Equip effect": "Надеть эффект",
"Buying…": "Покупка…",
"Cosmetics use GeoCoins for now. Payment options may be added later.": "Пока косметика покупается за GeoCoins. Позже могут появиться другие способы оплаты.",
"Redstone Pulse": "Импульс редстоуна",
"A redstone signal ripples around your portrait.": "Вокруг портрета пробегает импульс редстоуна.",
"Ender Orbit": "Орбита Края",
"Two ender sparks circle your portrait.": "Вокруг портрета вращаются две искры Края.",
"Nether Flame": "Пламя Незера",
"A warm nether flame flickers along the frame.": "По рамке мерцает жаркое пламя Незера.",
"Aurora Crown": "Корона сияния",
"A living aurora sweeps around your portrait.": "Вокруг портрета переливается северное сияние.",
"Log in": "Войти",
"Tools & downloads": "Инструменты",
"Moderation": "Модерация",

View File

@@ -36,6 +36,7 @@ export const prefixUpdateSchema = z
.strict();
export type ProfileAppearance = {
avatar_path?: string | null;
profile_effect?: string | null;
name_glow?: boolean;
glow_color?: string;
role?: string;
@@ -44,7 +45,7 @@ export type ProfileAppearance = {
custom_prefix_icon?: string;
};
export const appearanceColumns =
"avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon" as const;
"avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon" as const;
export type BadgeUser = ProfileAppearance & {
id: string;
display_name: string;

View File

@@ -0,0 +1,8 @@
export const profileCosmeticIds = [
"redstone-pulse",
"ender-orbit",
"nether-flame",
"aurora-crown",
] as const;
export type ProfileCosmeticId = (typeof profileCosmeticIds)[number];

View File

@@ -0,0 +1,41 @@
import { z } from "zod";
import { profileCosmeticIds } from "./profile-cosmetic-ids";
export const profileCosmeticIdSchema = z.enum(profileCosmeticIds);
export type { ProfileCosmeticId } from "./profile-cosmetic-ids";
export const profileCosmeticActionSchema = z.discriminatedUnion("action", [
z
.object({
action: z.literal("purchase"),
cosmeticId: profileCosmeticIdSchema,
})
.strict(),
z
.object({
action: z.literal("equip"),
cosmeticId: profileCosmeticIdSchema.nullable(),
})
.strict(),
]);
export const cosmeticShopStateSchema = z
.object({
balance: z.number().int().nonnegative(),
owned: z.array(profileCosmeticIdSchema),
equipped: profileCosmeticIdSchema.nullable(),
catalog: z.array(
z
.object({
id: profileCosmeticIdSchema,
name: z.string().min(1).max(48),
description: z.string().min(1).max(160),
price: z.number().int().positive(),
rarity: z.enum(["common", "rare", "epic", "legendary"]),
})
.strict(),
),
})
.strict();
export type CosmeticShopState = z.infer<typeof cosmeticShopStateSchema>;