Add profile photo uploads and custom difficulty support
This commit is contained in:
@@ -42,10 +42,14 @@ export const profileUpdateSchema = z
|
||||
.object({
|
||||
displayName: displayNameSchema.optional(),
|
||||
glowColor: glowColorSchema.optional(),
|
||||
avatarPath: z.string().max(128).nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(value) => value.displayName !== undefined || value.glowColor !== undefined,
|
||||
(value) =>
|
||||
value.displayName !== undefined ||
|
||||
value.glowColor !== undefined ||
|
||||
value.avatarPath !== undefined,
|
||||
"Choose a profile change",
|
||||
);
|
||||
export const initialPasswordSchema = z
|
||||
|
||||
@@ -151,6 +151,7 @@ export type Database = {
|
||||
revision_id: string;
|
||||
host_player_id: string;
|
||||
difficulty: string;
|
||||
difficulty_settings: Json;
|
||||
player_limit: number;
|
||||
status: string;
|
||||
current_round: number;
|
||||
@@ -162,6 +163,7 @@ export type Database = {
|
||||
revision_id: string;
|
||||
host_player_id: string;
|
||||
difficulty: string;
|
||||
difficulty_settings: Json;
|
||||
player_limit: number;
|
||||
status?: string;
|
||||
current_round?: number;
|
||||
@@ -176,6 +178,28 @@ export type Database = {
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
difficulty_presets: {
|
||||
Row: {
|
||||
created_at: string;
|
||||
id: string;
|
||||
name: string;
|
||||
seconds: number;
|
||||
zoom: boolean;
|
||||
};
|
||||
Insert: {
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
name: string;
|
||||
seconds: number;
|
||||
zoom: boolean;
|
||||
};
|
||||
Update: {
|
||||
name?: string;
|
||||
seconds?: number;
|
||||
zoom?: boolean;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
multiplayer_rounds: {
|
||||
Row: {
|
||||
room_id: string;
|
||||
@@ -476,6 +500,7 @@ export type Database = {
|
||||
created_at: string;
|
||||
current_round: number;
|
||||
difficulty: string;
|
||||
difficulty_settings: Json;
|
||||
id: string;
|
||||
owner_id: string | null;
|
||||
revision_id: string;
|
||||
@@ -485,6 +510,7 @@ export type Database = {
|
||||
created_at?: string;
|
||||
current_round?: number;
|
||||
difficulty: string;
|
||||
difficulty_settings: Json;
|
||||
id?: string;
|
||||
owner_id?: string | null;
|
||||
revision_id: string;
|
||||
@@ -494,6 +520,7 @@ export type Database = {
|
||||
created_at?: string;
|
||||
current_round?: number;
|
||||
difficulty?: string;
|
||||
difficulty_settings?: Json;
|
||||
id?: string;
|
||||
owner_id?: string | null;
|
||||
revision_id?: string;
|
||||
@@ -540,6 +567,7 @@ export type Database = {
|
||||
};
|
||||
profiles: {
|
||||
Row: AccountStatusFields & {
|
||||
avatar_path: string | null;
|
||||
glow_color: string;
|
||||
bug_finder: boolean;
|
||||
custom_prefix: string | null;
|
||||
@@ -551,6 +579,7 @@ export type Database = {
|
||||
role: string;
|
||||
};
|
||||
Insert: Partial<AccountStatusFields> & {
|
||||
avatar_path?: string | null;
|
||||
glow_color?: string;
|
||||
bug_finder?: boolean;
|
||||
custom_prefix?: string | null;
|
||||
@@ -562,6 +591,7 @@ export type Database = {
|
||||
role?: string;
|
||||
};
|
||||
Update: Partial<AccountStatusFields> & {
|
||||
avatar_path?: string | null;
|
||||
glow_color?: string;
|
||||
bug_finder?: boolean;
|
||||
custom_prefix?: string | null;
|
||||
|
||||
@@ -169,6 +169,16 @@
|
||||
"← My account": "← Мой аккаунт",
|
||||
"Your public profile.": "Твой публичный профиль.",
|
||||
"A name for the maps and places you share.": "Имя для карт и мест, которыми ты делишься.",
|
||||
"Profile photo": "Фото профиля",
|
||||
"JPG, PNG or WebP · Maximum size: 5 MB.": "JPG, PNG или WebP · Максимальный размер: 5 МБ.",
|
||||
"Images must be 5 MB or smaller.": "Изображение должно быть не больше 5 МБ.",
|
||||
"Choose a JPG, PNG or WebP image.": "Выбери изображение JPG, PNG или WebP.",
|
||||
"Could not upload your profile photo. Please try again.": "Не удалось загрузить фото профиля. Попробуй ещё раз.",
|
||||
"Upload your profile photo again": "Загрузи фото профиля ещё раз",
|
||||
"Choose a valid profile photo": "Выбери подходящее фото профиля",
|
||||
"Choose an image no larger than 5 MB": "Выбери изображение размером не больше 5 МБ",
|
||||
"Remove photo": "Удалить фото",
|
||||
"Profile photo saved.": "Фото профиля сохранено.",
|
||||
"2–32 characters. Display names do not have to be unique.": "От 2 до 32 символов. Имя не обязано быть уникальным.",
|
||||
"Used for sign-in and recovery. Only you can see it.": "Для входа и восстановления. Почту видишь только ты.",
|
||||
"Save display name": "Сохранить имя",
|
||||
@@ -230,6 +240,13 @@
|
||||
"Terrain of {name}": "Рельеф карты {name}",
|
||||
"YOUR EXPEDITION": "ТВОЯ ЭКСПЕДИЦИЯ",
|
||||
"Choose your challenge.": "Выбери сложность.",
|
||||
"Difficulty": "Сложность",
|
||||
"Create custom difficulty": "Создать свою сложность",
|
||||
"Difficulty name": "Название сложности",
|
||||
"Name this difficulty": "Например, «Хардкор»",
|
||||
"Round time": "Время на раунд",
|
||||
"Allow zoom": "Разрешить приближение",
|
||||
"Save difficulty": "Сохранить сложность",
|
||||
"s per round": "с на раунд",
|
||||
"Zoom allowed": "Можно приближать",
|
||||
"No zoom": "Без приближения",
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { z } from "zod";
|
||||
import { displayNameSchema } from "./account";
|
||||
import type { Bounds, Difficulty } from "./protocol";
|
||||
import type { Bounds, DifficultyPreset } from "./protocol";
|
||||
|
||||
export const createRoomSchema = z
|
||||
.object({
|
||||
datasetId: z.string().uuid(),
|
||||
difficulty: z.enum(["easy", "normal", "hard"]),
|
||||
difficulty: z.union([
|
||||
z.enum(["easy", "normal", "hard"]),
|
||||
z.string().uuid(),
|
||||
]),
|
||||
playerLimit: z.number().int().min(2).max(16),
|
||||
})
|
||||
.strict();
|
||||
@@ -53,7 +56,8 @@ export type RoomView = {
|
||||
id: string;
|
||||
datasetId: string;
|
||||
name: string;
|
||||
difficulty: Difficulty;
|
||||
difficulty: string;
|
||||
difficultySettings: DifficultyPreset;
|
||||
playerLimit: number;
|
||||
playerCount: number;
|
||||
status: "lobby" | "loading" | "playing" | "results" | "finished" | "closed";
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { z } from "zod";
|
||||
export const defaultGlowColor = "#ffe6a1";
|
||||
export const profileAvatarBucket = "minegeo-avatars";
|
||||
export const profileAvatarMaxBytes = 5 * 1024 * 1024;
|
||||
export const profileAvatarMimeTypes = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
] as const;
|
||||
export const glowColorSchema = z
|
||||
.string()
|
||||
.regex(/^#[a-fA-F0-9]{6}$/, "Choose a valid glow color")
|
||||
@@ -28,6 +35,7 @@ export const prefixUpdateSchema = z
|
||||
.object({ prefix: customPrefixSchema.nullable(), icon: z.enum(prefixIcons) })
|
||||
.strict();
|
||||
export type ProfileAppearance = {
|
||||
avatar_path?: string | null;
|
||||
name_glow?: boolean;
|
||||
glow_color?: string;
|
||||
role?: string;
|
||||
@@ -36,7 +44,7 @@ export type ProfileAppearance = {
|
||||
custom_prefix_icon?: string;
|
||||
};
|
||||
export const appearanceColumns =
|
||||
"name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon" as const;
|
||||
"avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon" as const;
|
||||
export type BadgeUser = ProfileAppearance & {
|
||||
id: string;
|
||||
display_name: string;
|
||||
@@ -44,3 +52,10 @@ export type BadgeUser = ProfileAppearance & {
|
||||
export function glowColor(value?: string) {
|
||||
return glowColorSchema.safeParse(value).data ?? defaultGlowColor;
|
||||
}
|
||||
|
||||
export function profileAvatarUrl(path?: string | null) {
|
||||
const base = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, "");
|
||||
if (!base || !path) return null;
|
||||
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
||||
return `${base}/storage/v1/object/public/${profileAvatarBucket}/${encodedPath}`;
|
||||
}
|
||||
|
||||
@@ -171,6 +171,35 @@ export const difficulties = {
|
||||
hard: { seconds: 30, zoom: false, label: "Hard" },
|
||||
} as const;
|
||||
export type Difficulty = keyof typeof difficulties;
|
||||
export type DifficultyPreset = {
|
||||
id: string;
|
||||
name: string;
|
||||
seconds: number;
|
||||
zoom: boolean;
|
||||
};
|
||||
export const customDifficultySchema = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2)
|
||||
.max(40)
|
||||
.refine(
|
||||
(name) =>
|
||||
!/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(
|
||||
name,
|
||||
),
|
||||
"Difficulty names cannot contain hidden controls",
|
||||
),
|
||||
seconds: z
|
||||
.number()
|
||||
.int()
|
||||
.min(15)
|
||||
.max(600)
|
||||
.refine((seconds) => seconds % 15 === 0),
|
||||
zoom: z.boolean(),
|
||||
})
|
||||
.strict();
|
||||
export function scoreGuess(
|
||||
a: Pick<Point, "x" | "z">,
|
||||
b: Pick<Point, "x" | "z">,
|
||||
|
||||
Reference in New Issue
Block a user