mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-14 12:12:13 +00:00
feat: moving visibility update to settings
This commit is contained in:
parent
383578bca2
commit
2b2b5afd79
51 changed files with 1096 additions and 10511 deletions
58
electron.vite.config.1726264954825.mjs
Normal file
58
electron.vite.config.1726264954825.mjs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// electron.vite.config.ts
|
||||||
|
import { resolve } from "path";
|
||||||
|
import {
|
||||||
|
defineConfig,
|
||||||
|
loadEnv,
|
||||||
|
swcPlugin,
|
||||||
|
externalizeDepsPlugin
|
||||||
|
} from "electron-vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||||
|
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
||||||
|
import svgr from "vite-plugin-svgr";
|
||||||
|
var sentryPlugin = sentryVitePlugin({
|
||||||
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
|
org: "hydra-launcher",
|
||||||
|
project: "hydra-launcher"
|
||||||
|
});
|
||||||
|
var electron_vite_config_default = defineConfig(({ mode }) => {
|
||||||
|
loadEnv(mode);
|
||||||
|
return {
|
||||||
|
main: {
|
||||||
|
build: {
|
||||||
|
sourcemap: true,
|
||||||
|
rollupOptions: {
|
||||||
|
external: ["better-sqlite3"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@main": resolve("src/main"),
|
||||||
|
"@locales": resolve("src/locales"),
|
||||||
|
"@resources": resolve("resources"),
|
||||||
|
"@shared": resolve("src/shared")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin]
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
build: {
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@renderer": resolve("src/renderer/src"),
|
||||||
|
"@locales": resolve("src/locales"),
|
||||||
|
"@shared": resolve("src/shared")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
export {
|
||||||
|
electron_vite_config_default as default
|
||||||
|
};
|
|
@ -35,6 +35,7 @@
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@fontsource/noto-sans": "^5.0.22",
|
"@fontsource/noto-sans": "^5.0.22",
|
||||||
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
"@sentry/electron": "^5.1.0",
|
"@sentry/electron": "^5.1.0",
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"parse-torrent": "^11.0.16",
|
"parse-torrent": "^11.0.16",
|
||||||
"piscina": "^4.5.1",
|
"piscina": "^4.5.1",
|
||||||
|
"react-hook-form": "^7.53.0",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"react-loading-skeleton": "^3.4.0",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
"react-redux": "^9.1.1",
|
"react-redux": "^9.1.1",
|
||||||
|
@ -73,6 +75,7 @@
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
"user-agents": "^1.1.193",
|
"user-agents": "^1.1.193",
|
||||||
"yaml": "^2.4.1",
|
"yaml": "^2.4.1",
|
||||||
|
"yup": "^1.4.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
9712
pnpm-lock.yaml
9712
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,9 @@
|
||||||
"trending": "Trending",
|
"trending": "Trending",
|
||||||
"surprise_me": "Surprise me",
|
"surprise_me": "Surprise me",
|
||||||
"no_results": "No results found",
|
"no_results": "No results found",
|
||||||
"start_typing": "Starting typing to search..."
|
"start_typing": "Starting typing to search...",
|
||||||
|
"hot": "🔥 Hot now",
|
||||||
|
"weekly": "📅 Top games of the week"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Catalogue",
|
"catalogue": "Catalogue",
|
||||||
|
@ -116,7 +118,14 @@
|
||||||
"download_paused": "Download paused",
|
"download_paused": "Download paused",
|
||||||
"last_downloaded_option": "Last downloaded option",
|
"last_downloaded_option": "Last downloaded option",
|
||||||
"create_shortcut_success": "Shortcut created successfully",
|
"create_shortcut_success": "Shortcut created successfully",
|
||||||
"create_shortcut_error": "Error creating shortcut"
|
"create_shortcut_error": "Error creating shortcut",
|
||||||
|
"nsfw_content_title": "This game contains innapropriate content",
|
||||||
|
"nsfw_content_description": "{{title}} contains content that may not be suitable for all ages. Are you sure you want to continue?",
|
||||||
|
"allow_nsfw_content": "Continue",
|
||||||
|
"refuse_nsfw_content": "Go back",
|
||||||
|
"stats": "Stats",
|
||||||
|
"download_count": "Downloads",
|
||||||
|
"player_count": "Active players"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
@ -192,7 +201,13 @@
|
||||||
"found_download_option_zero": "No download option found",
|
"found_download_option_zero": "No download option found",
|
||||||
"found_download_option_one": "Found {{countFormatted}} download option",
|
"found_download_option_one": "Found {{countFormatted}} download option",
|
||||||
"found_download_option_other": "Found {{countFormatted}} download options",
|
"found_download_option_other": "Found {{countFormatted}} download options",
|
||||||
"import": "Import"
|
"import": "Import",
|
||||||
|
"public": "Public",
|
||||||
|
"private": "Private",
|
||||||
|
"friends_only": "Friends only",
|
||||||
|
"privacy": "Privacy",
|
||||||
|
"profile_visibility": "Profile visibility",
|
||||||
|
"profile_visibility_description": "Choose who can see your profile and library"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
@ -262,11 +277,6 @@
|
||||||
"request_accepted": "Request accepted",
|
"request_accepted": "Request accepted",
|
||||||
"user_blocked_successfully": "User blocked successfully",
|
"user_blocked_successfully": "User blocked successfully",
|
||||||
"user_block_modal_text": "This will block {{displayName}}",
|
"user_block_modal_text": "This will block {{displayName}}",
|
||||||
"settings": "Settings",
|
|
||||||
"public": "Public",
|
|
||||||
"private": "Private",
|
|
||||||
"friends_only": "Friends only",
|
|
||||||
"privacy": "Privacy",
|
|
||||||
"blocked_users": "Blocked users",
|
"blocked_users": "Blocked users",
|
||||||
"unblock": "Unblock",
|
"unblock": "Unblock",
|
||||||
"no_friends_added": "You still don't have added friends",
|
"no_friends_added": "You still don't have added friends",
|
||||||
|
@ -274,6 +284,8 @@
|
||||||
"no_pending_invites": "You have no pending invites",
|
"no_pending_invites": "You have no pending invites",
|
||||||
"no_blocked_users": "You have no blocked users",
|
"no_blocked_users": "You have no blocked users",
|
||||||
"friend_code_copied": "Friend code copied",
|
"friend_code_copied": "Friend code copied",
|
||||||
"undo_friendship_modal_text": "This will undo your friendship with {{displayName}}"
|
"undo_friendship_modal_text": "This will undo your friendship with {{displayName}}",
|
||||||
|
"privacy_hint": "To adjust who can see this, go to the <0>Settings</0>",
|
||||||
|
"locked_profile": "This profile is private"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,14 @@
|
||||||
"download_paused": "Download pausado",
|
"download_paused": "Download pausado",
|
||||||
"last_downloaded_option": "Última opção baixada",
|
"last_downloaded_option": "Última opção baixada",
|
||||||
"create_shortcut_success": "Atalho criado com sucesso",
|
"create_shortcut_success": "Atalho criado com sucesso",
|
||||||
"create_shortcut_error": "Erro ao criar atalho"
|
"create_shortcut_error": "Erro ao criar atalho",
|
||||||
|
"nsfw_content_title": "Este jogo contém conteúdo inapropriado",
|
||||||
|
"nsfw_content_description": "{{title}} contém conteúdo que pode não ser apropriado para todas as idades. Você deseja continuar?",
|
||||||
|
"allow_nsfw_content": "Continuar",
|
||||||
|
"refuse_nsfw_content": "Voltar",
|
||||||
|
"stats": "Estatísticas",
|
||||||
|
"download_count": "Downloads",
|
||||||
|
"player_count": "Jogadores ativos"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
|
@ -193,7 +200,13 @@
|
||||||
"found_download_option_zero": "Nenhuma opção de download encontrada",
|
"found_download_option_zero": "Nenhuma opção de download encontrada",
|
||||||
"found_download_option_one": "{{countFormatted}} opção de download encontrada",
|
"found_download_option_one": "{{countFormatted}} opção de download encontrada",
|
||||||
"found_download_option_other": "{{countFormatted}} opções de download encontradas",
|
"found_download_option_other": "{{countFormatted}} opções de download encontradas",
|
||||||
"import": "Importar"
|
"import": "Importar",
|
||||||
|
"privacy": "Privacidade",
|
||||||
|
"private": "Privado",
|
||||||
|
"friends_only": "Apenas amigos",
|
||||||
|
"public": "Público",
|
||||||
|
"profile_visibility": "Visibilidade do perfil",
|
||||||
|
"profile_visibility_description": "Escolha quem pode ver seu perfil e biblioteca"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
@ -267,11 +280,6 @@
|
||||||
"request_accepted": "Pedido de amizade aceito",
|
"request_accepted": "Pedido de amizade aceito",
|
||||||
"user_blocked_successfully": "Usuário bloqueado com sucesso",
|
"user_blocked_successfully": "Usuário bloqueado com sucesso",
|
||||||
"user_block_modal_text": "Bloquear {{displayName}}",
|
"user_block_modal_text": "Bloquear {{displayName}}",
|
||||||
"settings": "Configurações",
|
|
||||||
"privacy": "Privacidade",
|
|
||||||
"private": "Privado",
|
|
||||||
"friends_only": "Apenas amigos",
|
|
||||||
"public": "Público",
|
|
||||||
"blocked_users": "Usuários bloqueados",
|
"blocked_users": "Usuários bloqueados",
|
||||||
"unblock": "Desbloquear",
|
"unblock": "Desbloquear",
|
||||||
"no_friends_added": "Você ainda não possui amigos adicionados",
|
"no_friends_added": "Você ainda não possui amigos adicionados",
|
||||||
|
@ -279,6 +287,8 @@
|
||||||
"no_pending_invites": "Você não possui convites de amizade pendentes",
|
"no_pending_invites": "Você não possui convites de amizade pendentes",
|
||||||
"no_blocked_users": "Você não tem nenhum usuário bloqueado",
|
"no_blocked_users": "Você não tem nenhum usuário bloqueado",
|
||||||
"friend_code_copied": "Código de amigo copiado",
|
"friend_code_copied": "Código de amigo copiado",
|
||||||
"undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}"
|
"undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}",
|
||||||
|
"privacy_hint": "Pra controlar quem pode ver seu perfil, acesse a <0>Tela de Configurações</0>",
|
||||||
|
"profile_locked": "Este perfil é privado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import { HydraApi, PythonInstance, gamesPlaytime } from "@main/services";
|
import {
|
||||||
|
DownloadManager,
|
||||||
|
HydraApi,
|
||||||
|
PythonInstance,
|
||||||
|
gamesPlaytime,
|
||||||
|
} from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import { DownloadQueue, Game, UserAuth } from "@main/entity";
|
import { DownloadQueue, Game, UserAuth } from "@main/entity";
|
||||||
|
|
||||||
|
@ -23,6 +28,9 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
/* Removes user from Sentry */
|
/* Removes user from Sentry */
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
|
|
||||||
|
/* Cancels any ongoing downloads */
|
||||||
|
DownloadManager.cancelDownload();
|
||||||
|
|
||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
PythonInstance.killTorrent();
|
PythonInstance.killTorrent();
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ const getCatalogue = async (
|
||||||
|
|
||||||
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
|
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
|
||||||
`/games/${category}?${params.toString()}`,
|
`/games/${category}?${params.toString()}`,
|
||||||
|
{},
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { GenericHttpDownloader } from "./generic-http-downloader";
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
private static currentDownloader: Downloader | null = null;
|
private static currentDownloader: Downloader | null = null;
|
||||||
|
private static downloadingGameId: number | null = null;
|
||||||
|
|
||||||
public static async watchDownloads() {
|
public static async watchDownloads() {
|
||||||
let status: DownloadProgress | null = null;
|
let status: DownloadProgress | null = null;
|
||||||
|
@ -76,13 +77,14 @@ export class DownloadManager {
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
this.currentDownloader = null;
|
this.currentDownloader = null;
|
||||||
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeDownload(game: Game) {
|
static async resumeDownload(game: Game) {
|
||||||
return this.startDownload(game);
|
return this.startDownload(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload(gameId: number) {
|
static async cancelDownload(gameId = this.downloadingGameId!) {
|
||||||
if (this.currentDownloader === Downloader.Torrent) {
|
if (this.currentDownloader === Downloader.Torrent) {
|
||||||
PythonInstance.cancelDownload(gameId);
|
PythonInstance.cancelDownload(gameId);
|
||||||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
||||||
|
@ -93,6 +95,7 @@ export class DownloadManager {
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
this.currentDownloader = null;
|
this.currentDownloader = null;
|
||||||
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async startDownload(game: Game) {
|
static async startDownload(game: Game) {
|
||||||
|
@ -131,5 +134,6 @@ export class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentDownloader = game.downloader;
|
this.currentDownloader = game.downloader;
|
||||||
|
this.downloadingGameId = game.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,3 +81,5 @@ export const publishNotificationUpdateReadyToInstall = async (
|
||||||
icon: trayIcon,
|
icon: trayIcon,
|
||||||
}).show();
|
}).show();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const publishNewFriendRequestNotification = async () => {};
|
||||||
|
|
|
@ -58,11 +58,11 @@ export const button = styleVariants({
|
||||||
danger: [
|
danger: [
|
||||||
base,
|
base,
|
||||||
{
|
{
|
||||||
border: `solid 1px #a31533`,
|
borderColor: "transparent",
|
||||||
backgroundColor: "transparent",
|
|
||||||
color: "white",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "#a31533",
|
backgroundColor: "#a31533",
|
||||||
|
color: "#c0c1c7",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "#b3203f",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -3,26 +3,44 @@ import { Modal, type ModalProps } from "../modal/modal";
|
||||||
|
|
||||||
import * as styles from "./confirmation-modal.css";
|
import * as styles from "./confirmation-modal.css";
|
||||||
|
|
||||||
export interface ConfirmationModalProps extends ModalProps {
|
export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
|
||||||
confirmButtonLabel: string;
|
confirmButtonLabel: string;
|
||||||
cancelButtonLabel: string;
|
cancelButtonLabel: string;
|
||||||
descriptionText: string;
|
descriptionText: string;
|
||||||
|
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfirmationModal({
|
export function ConfirmationModal({
|
||||||
confirmButtonLabel,
|
confirmButtonLabel,
|
||||||
cancelButtonLabel,
|
cancelButtonLabel,
|
||||||
descriptionText,
|
descriptionText,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
...props
|
...props
|
||||||
}: ConfirmationModalProps) {
|
}: ConfirmationModalProps) {
|
||||||
|
const handleCancelClick = () => {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal {...props}>
|
<Modal {...props}>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
||||||
<p className={styles.descriptionText}>{descriptionText}</p>
|
<p className={styles.descriptionText}>{descriptionText}</p>
|
||||||
|
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Button theme="danger">{cancelButtonLabel}</Button>
|
<Button theme="outline" onClick={handleCancelClick}>
|
||||||
<Button>{confirmButtonLabel}</Button>
|
{cancelButtonLabel}
|
||||||
|
</Button>
|
||||||
|
<Button theme="danger" onClick={onConfirm}>
|
||||||
|
{confirmButtonLabel}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -6,7 +6,8 @@ import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import * as styles from "./game-card.css";
|
import * as styles from "./game-card.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Badge } from "../badge/badge";
|
import { Badge } from "../badge/badge";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
import { useFormat } from "@renderer/hooks";
|
||||||
|
|
||||||
export interface GameCardProps
|
export interface GameCardProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
|
@ -25,8 +26,6 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||||
|
|
||||||
const [stats, setStats] = useState<GameStats | null>(null);
|
const [stats, setStats] = useState<GameStats | null>(null);
|
||||||
|
|
||||||
const { i18n } = useTranslation();
|
|
||||||
|
|
||||||
const uniqueRepackers = Array.from(
|
const uniqueRepackers = Array.from(
|
||||||
new Set(game.repacks.map(({ repacker }) => repacker))
|
new Set(game.repacks.map(({ repacker }) => repacker))
|
||||||
);
|
);
|
||||||
|
@ -39,11 +38,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||||
}
|
}
|
||||||
}, [game, stats]);
|
}, [game, stats]);
|
||||||
|
|
||||||
const numberFormatter = useMemo(() => {
|
const { numberFormatter } = useFormat();
|
||||||
return new Intl.NumberFormat(i18n.language, {
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
});
|
|
||||||
}, [i18n.language]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -25,6 +25,9 @@ export const sidebar = recipe({
|
||||||
true: {
|
true: {
|
||||||
paddingTop: `${SPACING_UNIT * 6}px`,
|
paddingTop: `${SPACING_UNIT * 6}px`,
|
||||||
},
|
},
|
||||||
|
false: {
|
||||||
|
paddingTop: `${SPACING_UNIT * 2}px`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -149,7 +149,6 @@ export function Sidebar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<aside
|
<aside
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
className={styles.sidebar({
|
className={styles.sidebar({
|
||||||
|
@ -202,8 +201,7 @@ export function Sidebar() {
|
||||||
key={game.id}
|
key={game.id}
|
||||||
className={styles.menuItem({
|
className={styles.menuItem({
|
||||||
active:
|
active:
|
||||||
location.pathname ===
|
location.pathname === `/game/${game.shop}/${game.objectID}`,
|
||||||
`/game/${game.shop}/${game.objectID}`,
|
|
||||||
muted: game.status === "removed",
|
muted: game.status === "removed",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -238,6 +236,5 @@ export function Sidebar() {
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,16 +22,6 @@ export const textField = recipe({
|
||||||
minHeight: "40px",
|
minHeight: "40px",
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
focused: {
|
|
||||||
true: {
|
|
||||||
borderColor: "#DADBE1",
|
|
||||||
},
|
|
||||||
false: {
|
|
||||||
":hover": {
|
|
||||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
theme: {
|
theme: {
|
||||||
primary: {
|
primary: {
|
||||||
backgroundColor: vars.color.darkBackground,
|
backgroundColor: vars.color.darkBackground,
|
||||||
|
@ -40,11 +30,21 @@ export const textField = recipe({
|
||||||
backgroundColor: vars.color.background,
|
backgroundColor: vars.color.background,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
state: {
|
hasError: {
|
||||||
error: {
|
true: {
|
||||||
borderColor: vars.color.danger,
|
borderColor: vars.color.danger,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
focused: {
|
||||||
|
true: {
|
||||||
|
borderColor: "#DADBE1",
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
":hover": {
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.5)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -83,3 +83,7 @@ export const textFieldWrapper = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const errorLabel = style({
|
||||||
|
color: vars.color.danger,
|
||||||
|
});
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import React, { useId, useMemo, useState } from "react";
|
import React, { useId, useMemo, useState } from "react";
|
||||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
||||||
import * as styles from "./text-field.css";
|
import type { FieldError, FieldErrorsImpl, Merge } from "react-hook-form";
|
||||||
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import * as styles from "./text-field.css";
|
||||||
|
|
||||||
export interface TextFieldProps
|
export interface TextFieldProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
@ -21,23 +23,27 @@ export interface TextFieldProps
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
>;
|
>;
|
||||||
rightContent?: React.ReactNode | null;
|
rightContent?: React.ReactNode | null;
|
||||||
state?: NonNullable<RecipeVariants<typeof styles.textField>>["state"];
|
error?: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TextField({
|
export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
theme = "primary",
|
theme = "primary",
|
||||||
label,
|
label,
|
||||||
hint,
|
hint,
|
||||||
textFieldProps,
|
textFieldProps,
|
||||||
containerProps,
|
containerProps,
|
||||||
rightContent = null,
|
rightContent = null,
|
||||||
state,
|
error,
|
||||||
...props
|
...props
|
||||||
}: TextFieldProps) {
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
|
|
||||||
const { t } = useTranslation("forms");
|
const { t } = useTranslation("forms");
|
||||||
|
|
||||||
|
@ -48,21 +54,48 @@ export function TextField({
|
||||||
return props.type ?? "text";
|
return props.type ?? "text";
|
||||||
}, [props.type, isPasswordVisible]);
|
}, [props.type, isPasswordVisible]);
|
||||||
|
|
||||||
|
const hintContent = useMemo(() => {
|
||||||
|
if (error && error.message)
|
||||||
|
return (
|
||||||
|
<small className={styles.errorLabel}>{error.message as string}</small>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hint) return <small>{hint}</small>;
|
||||||
|
return null;
|
||||||
|
}, [hint, error]);
|
||||||
|
|
||||||
|
const handleFocus: React.FocusEventHandler<HTMLInputElement> = (event) => {
|
||||||
|
setIsFocused(true);
|
||||||
|
if (props.onFocus) props.onFocus(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (event) => {
|
||||||
|
setIsFocused(false);
|
||||||
|
if (props.onBlur) props.onBlur(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasError = !!error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.textFieldContainer} {...containerProps}>
|
<div className={styles.textFieldContainer} {...containerProps}>
|
||||||
{label && <label htmlFor={id}>{label}</label>}
|
{label && <label htmlFor={id}>{label}</label>}
|
||||||
|
|
||||||
<div className={styles.textFieldWrapper}>
|
<div className={styles.textFieldWrapper}>
|
||||||
<div
|
<div
|
||||||
className={styles.textField({ focused: isFocused, theme, state })}
|
className={styles.textField({
|
||||||
|
theme,
|
||||||
|
hasError,
|
||||||
|
focused: isFocused,
|
||||||
|
})}
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
ref={ref}
|
||||||
id={id}
|
id={id}
|
||||||
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
||||||
onFocus={() => setIsFocused(true)}
|
|
||||||
onBlur={() => setIsFocused(false)}
|
|
||||||
{...props}
|
{...props}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
type={inputType}
|
type={inputType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -85,7 +118,10 @@ export function TextField({
|
||||||
{rightContent}
|
{rightContent}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hint && <small>{hint}</small>}
|
{hintContent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TextField.displayName = "TextField";
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { createContext, useCallback, useEffect, useState } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useParams, useSearchParams } from "react-router-dom";
|
import { useParams, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
|
@ -15,6 +21,7 @@ import type {
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { GameDetailsContext } from "./game-details.context.types";
|
import { GameDetailsContext } from "./game-details.context.types";
|
||||||
|
import { SteamContentDescriptor } from "@shared";
|
||||||
|
|
||||||
export const gameDetailsContext = createContext<GameDetailsContext>({
|
export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||||
game: null,
|
game: null,
|
||||||
|
@ -29,11 +36,13 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||||
showRepacksModal: false,
|
showRepacksModal: false,
|
||||||
showGameOptionsModal: false,
|
showGameOptionsModal: false,
|
||||||
stats: null,
|
stats: null,
|
||||||
|
hasNSFWContentBlocked: false,
|
||||||
setGameColor: () => {},
|
setGameColor: () => {},
|
||||||
selectGameExecutable: async () => null,
|
selectGameExecutable: async () => null,
|
||||||
updateGame: async () => {},
|
updateGame: async () => {},
|
||||||
setShowGameOptionsModal: () => {},
|
setShowGameOptionsModal: () => {},
|
||||||
setShowRepacksModal: () => {},
|
setShowRepacksModal: () => {},
|
||||||
|
setHasNSFWContentBlocked: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Provider } = gameDetailsContext;
|
const { Provider } = gameDetailsContext;
|
||||||
|
@ -48,9 +57,10 @@ export function GameDetailsContextProvider({
|
||||||
}: GameDetailsContextProps) {
|
}: GameDetailsContextProps) {
|
||||||
const { objectID, shop } = useParams();
|
const { objectID, shop } = useParams();
|
||||||
|
|
||||||
const [shopDetails, setGameDetails] = useState<ShopDetails | null>(null);
|
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
||||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||||
const [game, setGame] = useState<Game | null>(null);
|
const [game, setGame] = useState<Game | null>(null);
|
||||||
|
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
|
||||||
|
|
||||||
const [stats, setStats] = useState<GameStats | null>(null);
|
const [stats, setStats] = useState<GameStats | null>(null);
|
||||||
|
|
||||||
|
@ -97,8 +107,17 @@ export function GameDetailsContextProvider({
|
||||||
window.electron.getGameStats(objectID!, shop as GameShop),
|
window.electron.getGameStats(objectID!, shop as GameShop),
|
||||||
])
|
])
|
||||||
.then(([appDetailsResult, repacksResult, statsResult]) => {
|
.then(([appDetailsResult, repacksResult, statsResult]) => {
|
||||||
if (appDetailsResult.status === "fulfilled")
|
if (appDetailsResult.status === "fulfilled") {
|
||||||
setGameDetails(appDetailsResult.value);
|
setShopDetails(appDetailsResult.value);
|
||||||
|
|
||||||
|
if (
|
||||||
|
appDetailsResult.value!.content_descriptors.ids.includes(
|
||||||
|
SteamContentDescriptor.AdultOnlySexualContent
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setHasNSFWContentBlocked(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (repacksResult.status === "fulfilled")
|
if (repacksResult.status === "fulfilled")
|
||||||
setRepacks(repacksResult.value);
|
setRepacks(repacksResult.value);
|
||||||
|
@ -113,7 +132,7 @@ export function GameDetailsContextProvider({
|
||||||
}, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]);
|
}, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGameDetails(null);
|
setShopDetails(null);
|
||||||
setGame(null);
|
setGame(null);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setisGameRunning(false);
|
setisGameRunning(false);
|
||||||
|
@ -180,6 +199,8 @@ export function GameDetailsContextProvider({
|
||||||
showGameOptionsModal,
|
showGameOptionsModal,
|
||||||
showRepacksModal,
|
showRepacksModal,
|
||||||
stats,
|
stats,
|
||||||
|
hasNSFWContentBlocked,
|
||||||
|
setHasNSFWContentBlocked,
|
||||||
setGameColor,
|
setGameColor,
|
||||||
selectGameExecutable,
|
selectGameExecutable,
|
||||||
updateGame,
|
updateGame,
|
||||||
|
|
|
@ -19,9 +19,11 @@ export interface GameDetailsContext {
|
||||||
showRepacksModal: boolean;
|
showRepacksModal: boolean;
|
||||||
showGameOptionsModal: boolean;
|
showGameOptionsModal: boolean;
|
||||||
stats: GameStats | null;
|
stats: GameStats | null;
|
||||||
|
hasNSFWContentBlocked: boolean;
|
||||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||||
selectGameExecutable: () => Promise<string | null>;
|
selectGameExecutable: () => Promise<string | null>;
|
||||||
updateGame: () => Promise<void>;
|
updateGame: () => Promise<void>;
|
||||||
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
setShowGameOptionsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowGameOptionsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setHasNSFWContentBlocked: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,4 @@ export * from "./use-date";
|
||||||
export * from "./use-toast";
|
export * from "./use-toast";
|
||||||
export * from "./redux";
|
export * from "./redux";
|
||||||
export * from "./use-user-details";
|
export * from "./use-user-details";
|
||||||
|
export * from "./use-format";
|
||||||
|
|
14
src/renderer/src/hooks/use-format.ts
Normal file
14
src/renderer/src/hooks/use-format.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export function useFormat() {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
const numberFormatter = useMemo(() => {
|
||||||
|
return new Intl.NumberFormat(i18n.language, {
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
});
|
||||||
|
}, [i18n.language]);
|
||||||
|
|
||||||
|
return { numberFormatter };
|
||||||
|
}
|
|
@ -88,7 +88,7 @@ export function useUserDetails() {
|
||||||
[updateUserDetails]
|
[updateUserDetails]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchFriendRequests = useCallback(() => {
|
const fetchFriendRequests = useCallback(async () => {
|
||||||
return window.electron
|
return window.electron
|
||||||
.getFriendRequests()
|
.getFriendRequests()
|
||||||
.then((friendRequests) => {
|
.then((friendRequests) => {
|
||||||
|
@ -127,13 +127,10 @@ export function useUserDetails() {
|
||||||
[fetchFriendRequests]
|
[fetchFriendRequests]
|
||||||
);
|
);
|
||||||
|
|
||||||
const undoFriendship = (userId: string) => {
|
const undoFriendship = (userId: string) =>
|
||||||
return window.electron.undoFriendship(userId);
|
window.electron.undoFriendship(userId);
|
||||||
};
|
|
||||||
|
|
||||||
const blockUser = (userId: string) => {
|
const blockUser = (userId: string) => window.electron.blockUser(userId);
|
||||||
return window.electron.blockUser(userId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const unblockUser = (userId: string) => {
|
const unblockUser = (userId: string) => {
|
||||||
return window.electron.unblockUser(userId);
|
return window.electron.unblockUser(userId);
|
||||||
|
|
|
@ -21,8 +21,14 @@ export function GameDetailsContent() {
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const { objectID, shopDetails, game, gameColor, setGameColor } =
|
const {
|
||||||
useContext(gameDetailsContext);
|
objectID,
|
||||||
|
shopDetails,
|
||||||
|
game,
|
||||||
|
gameColor,
|
||||||
|
setGameColor,
|
||||||
|
hasNSFWContentBlocked,
|
||||||
|
} = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const [backdropOpactiy, setBackdropOpacity] = useState(1);
|
const [backdropOpactiy, setBackdropOpacity] = useState(1);
|
||||||
|
|
||||||
|
@ -64,7 +70,7 @@ export function GameDetailsContent() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.libraryHero(objectID!)}
|
src={steamUrlBuilder.libraryHero(objectID!)}
|
||||||
className={styles.heroImage}
|
className={styles.heroImage}
|
||||||
|
@ -93,7 +99,7 @@ export function GameDetailsContent() {
|
||||||
<div className={styles.heroContent}>
|
<div className={styles.heroContent}>
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.logo(objectID!)}
|
src={steamUrlBuilder.logo(objectID!)}
|
||||||
style={{ width: 300, alignSelf: "flex-end" }}
|
className={styles.gameLogo}
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { globalStyle, keyframes, style } from "@vanilla-extract/css";
|
import { globalStyle, keyframes, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
|
||||||
export const HERO_HEIGHT = 300;
|
export const HERO_HEIGHT = 300;
|
||||||
|
|
||||||
|
@ -9,12 +10,22 @@ export const slideIn = keyframes({
|
||||||
"100%": { transform: "translateY(0)" },
|
"100%": { transform: "translateY(0)" },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const wrapper = style({
|
export const wrapper = recipe({
|
||||||
|
base: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
transition: "all ease 0.3s",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
blurredContent: {
|
||||||
|
true: {
|
||||||
|
filter: "blur(20px)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const hero = style({
|
export const hero = style({
|
||||||
|
@ -68,6 +79,11 @@ export const heroImage = style({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const gameLogo = style({
|
||||||
|
width: 300,
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
});
|
||||||
|
|
||||||
export const heroImageSkeleton = style({
|
export const heroImageSkeleton = style({
|
||||||
height: "300px",
|
height: "300px",
|
||||||
"@media": {
|
"@media": {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { GameRepack, GameShop, Steam250Game } from "@types";
|
import { GameRepack, GameShop, Steam250Game } from "@types";
|
||||||
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button, ConfirmationModal } from "@renderer/components";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||||
|
@ -83,6 +83,8 @@ export function GameDetails() {
|
||||||
shop,
|
shop,
|
||||||
showRepacksModal,
|
showRepacksModal,
|
||||||
showGameOptionsModal,
|
showGameOptionsModal,
|
||||||
|
hasNSFWContentBlocked,
|
||||||
|
setHasNSFWContentBlocked,
|
||||||
updateGame,
|
updateGame,
|
||||||
setShowRepacksModal,
|
setShowRepacksModal,
|
||||||
setShowGameOptionsModal,
|
setShowGameOptionsModal,
|
||||||
|
@ -107,6 +109,10 @@ export function GameDetails() {
|
||||||
setShowGameOptionsModal(false);
|
setShowGameOptionsModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNSFWContentRefuse = () => {
|
||||||
|
navigate(-1);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme
|
<SkeletonTheme
|
||||||
baseColor={vars.color.background}
|
baseColor={vars.color.background}
|
||||||
|
@ -120,6 +126,19 @@ export function GameDetails() {
|
||||||
onClose={() => setShowRepacksModal(false)}
|
onClose={() => setShowRepacksModal(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
visible={hasNSFWContentBlocked}
|
||||||
|
onClose={handleNSFWContentRefuse}
|
||||||
|
title={t("nsfw_content_title")}
|
||||||
|
descriptionText={t("nsfw_content_description", {
|
||||||
|
title: gameTitle,
|
||||||
|
})}
|
||||||
|
confirmButtonLabel={t("allow_nsfw_content")}
|
||||||
|
cancelButtonLabel={t("refuse_nsfw_content")}
|
||||||
|
onConfirm={() => setHasNSFWContentBlocked(false)}
|
||||||
|
clickOutsideToClose={false}
|
||||||
|
/>
|
||||||
|
|
||||||
{game && (
|
{game && (
|
||||||
<GameOptionsModal
|
<GameOptionsModal
|
||||||
visible={showGameOptionsModal}
|
visible={showGameOptionsModal}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "./hero-panel.css";
|
import * as styles from "./hero-panel.css";
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
import { useDate, useDownload } from "@renderer/hooks";
|
import { useDate, useDownload, useFormat } from "@renderer/hooks";
|
||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
@ -13,7 +13,9 @@ export function HeroPanelPlaytime() {
|
||||||
|
|
||||||
const { game, isGameRunning } = useContext(gameDetailsContext);
|
const { game, isGameRunning } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { i18n, t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
const { progress, lastPacket } = useDownload();
|
const { progress, lastPacket } = useDownload();
|
||||||
|
|
||||||
|
@ -29,12 +31,6 @@ export function HeroPanelPlaytime() {
|
||||||
}
|
}
|
||||||
}, [game?.lastTimePlayed, formatDistance]);
|
}, [game?.lastTimePlayed, formatDistance]);
|
||||||
|
|
||||||
const numberFormatter = useMemo(() => {
|
|
||||||
return new Intl.NumberFormat(i18n.language, {
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
});
|
|
||||||
}, [i18n.language]);
|
|
||||||
|
|
||||||
const formattedPlayTime = useMemo(() => {
|
const formattedPlayTime = useMemo(() => {
|
||||||
const milliseconds = game?.playTimeInMilliseconds || 0;
|
const milliseconds = game?.playTimeInMilliseconds || 0;
|
||||||
const seconds = milliseconds / 1000;
|
const seconds = milliseconds / 1000;
|
||||||
|
|
|
@ -70,7 +70,7 @@ export const howLongToBeatCategory = style({
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "4px",
|
gap: "4px",
|
||||||
backgroundColor: vars.color.background,
|
backgroundColor: vars.color.background,
|
||||||
borderRadius: "8px",
|
borderRadius: "4px",
|
||||||
padding: `8px 16px`,
|
padding: `8px 16px`,
|
||||||
border: `solid 1px ${vars.color.border}`,
|
border: `solid 1px ${vars.color.border}`,
|
||||||
});
|
});
|
||||||
|
@ -81,10 +81,32 @@ export const howLongToBeatCategoryLabel = style({
|
||||||
|
|
||||||
export const howLongToBeatCategorySkeleton = style({
|
export const howLongToBeatCategorySkeleton = style({
|
||||||
border: `solid 1px ${vars.color.border}`,
|
border: `solid 1px ${vars.color.border}`,
|
||||||
borderRadius: "8px",
|
borderRadius: "4px",
|
||||||
height: "76px",
|
height: "76px",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const statsSection = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const statsCategoryTitle = style({
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const statsCategory = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
alignItems: "flex-end",
|
||||||
|
});
|
||||||
|
|
||||||
globalStyle(`${requirementsDetails} a`, {
|
globalStyle(`${requirementsDetails} a`, {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
color: vars.color.body,
|
color: vars.color.body,
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { Button } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
import * as styles from "./sidebar.css";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
import { useFormat } from "@renderer/hooks";
|
||||||
|
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||||
|
@ -21,6 +23,8 @@ export function Sidebar() {
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (objectID) {
|
if (objectID) {
|
||||||
setHowLongToBeat({ isLoading: true, data: null });
|
setHowLongToBeat({ isLoading: true, data: null });
|
||||||
|
@ -43,18 +47,41 @@ export function Sidebar() {
|
||||||
isLoading={howLongToBeat.isLoading}
|
isLoading={howLongToBeat.isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.contentSidebarTitle} style={{ border: "none" }}>
|
{stats && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={styles.contentSidebarTitle}
|
||||||
|
style={{ border: "none" }}
|
||||||
|
>
|
||||||
<h3>{t("stats")}</h3>
|
<h3>{t("stats")}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className={styles.statsSection}>
|
||||||
<p>downloadCount {stats?.downloadCount}</p>
|
<div className={styles.statsCategory}>
|
||||||
<p>playerCount {stats?.playerCount}</p>
|
<p className={styles.statsCategoryTitle}>
|
||||||
|
<DownloadIcon size={18} />
|
||||||
|
{t("download_count")}
|
||||||
|
</p>
|
||||||
|
<p>{numberFormatter.format(stats?.downloadCount)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.contentSidebarTitle} style={{ border: "none" }}>
|
<div className={styles.statsCategory}>
|
||||||
|
<p className={styles.statsCategoryTitle}>
|
||||||
|
<PeopleIcon size={18} />
|
||||||
|
{t("player_count")}
|
||||||
|
</p>
|
||||||
|
<p>{numberFormatter.format(stats?.playerCount)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.contentSidebarTitle}
|
||||||
|
style={{ border: "none" }}
|
||||||
|
>
|
||||||
<h3>{t("requirements")}</h3>
|
<h3>{t("requirements")}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.requirementButtonContainer}>
|
<div className={styles.requirementButtonContainer}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -67,6 +67,12 @@ export function Home() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCategoryClick = (category: CatalogueCategory) => {
|
||||||
|
if (category !== currentCatalogueCategory) {
|
||||||
|
getCatalogue(category);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
getCatalogue(CatalogueCategory.Hot);
|
getCatalogue(CatalogueCategory.Hot);
|
||||||
|
@ -93,7 +99,7 @@ export function Home() {
|
||||||
? "primary"
|
? "primary"
|
||||||
: "outline"
|
: "outline"
|
||||||
}
|
}
|
||||||
onClick={() => getCatalogue(category)}
|
onClick={() => handleCategoryClick(category)}
|
||||||
>
|
>
|
||||||
{t(category)}
|
{t(category)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type { DebouncedFunc } from "lodash";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
|
|
||||||
import { InboxIcon, SearchIcon } from "@primer/octicons-react";
|
import { InboxIcon, SearchIcon } from "@primer/octicons-react";
|
||||||
import { clearSearch } from "@renderer/features";
|
import { clearSearch, setSearch } from "@renderer/features";
|
||||||
import { useAppDispatch } from "@renderer/hooks";
|
import { useAppDispatch } from "@renderer/hooks";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -37,6 +37,10 @@ export function SearchResults() {
|
||||||
navigate(buildGameDetailsPath(game));
|
navigate(buildGameDetailsPath(game));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setSearch(searchParams.get("query") ?? ""));
|
||||||
|
}, [dispatch, searchParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (debouncedFunc.current) debouncedFunc.current.cancel();
|
if (debouncedFunc.current) debouncedFunc.current.cancel();
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { vars } from "../../../theme.css";
|
||||||
|
import { globalStyle, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
export const profileAvatarEditContainer = style({
|
||||||
|
alignSelf: "center",
|
||||||
|
width: "128px",
|
||||||
|
height: "128px",
|
||||||
|
display: "flex",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: vars.color.body,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
position: "relative",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||||
|
cursor: "pointer",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileAvatar = style({
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: "4px",
|
||||||
|
overflow: "hidden",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileAvatarEditOverlay = style({
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||||
|
color: vars.color.muted,
|
||||||
|
zIndex: "1",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
alignItems: "center",
|
||||||
|
opacity: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
globalStyle(`${profileAvatarEditContainer}:hover ${profileAvatarEditOverlay}`, {
|
||||||
|
opacity: "1",
|
||||||
|
});
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { useContext, useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Link,
|
||||||
|
Modal,
|
||||||
|
ModalProps,
|
||||||
|
TextField,
|
||||||
|
} from "@renderer/components";
|
||||||
|
import { useAppSelector, useToast, useUserDetails } from "@renderer/hooks";
|
||||||
|
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import * as styles from "./edit-profile-modal.css";
|
||||||
|
import { userProfileContext } from "@renderer/context";
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
profileImageUrl?: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditProfileModal(
|
||||||
|
props: Omit<ModalProps, "children" | "title">
|
||||||
|
) {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
displayName: yup
|
||||||
|
.string()
|
||||||
|
.required(t("required_field"))
|
||||||
|
.min(3, t("displayname_min_length"))
|
||||||
|
.max(50, t("displayname_max_length")),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, errors },
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getUserProfile } = useContext(userProfileContext);
|
||||||
|
|
||||||
|
const { userDetails } = useAppSelector((state) => state.userDetails);
|
||||||
|
const { fetchUserDetails } = useUserDetails();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userDetails) {
|
||||||
|
setValue("displayName", userDetails.displayName);
|
||||||
|
}
|
||||||
|
}, [setValue, userDetails]);
|
||||||
|
|
||||||
|
const { patchUser } = useUserDetails();
|
||||||
|
|
||||||
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const onSubmit = async (values: FormValues) => {
|
||||||
|
return patchUser(values)
|
||||||
|
.then(async () => {
|
||||||
|
await Promise.allSettled([fetchUserDetails(), getUserProfile()]);
|
||||||
|
showSuccessToast(t("saved_successfully"));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast(t("try_again"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal {...props} title={t("edit_profile")} clickOutsideToClose={false}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "350px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="profileImageUrl"
|
||||||
|
render={({ field: { value, onChange } }) => {
|
||||||
|
const handleChangeProfileAvatar = async () => {
|
||||||
|
const { filePaths } = await window.electron.showOpenDialog({
|
||||||
|
properties: ["openFile"],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "Image",
|
||||||
|
extensions: ["jpg", "jpeg", "png"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePaths && filePaths.length > 0) {
|
||||||
|
const path = filePaths[0];
|
||||||
|
|
||||||
|
onChange(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageUrl = () => {
|
||||||
|
if (value) return `local:${value}`;
|
||||||
|
if (userDetails?.profileImageUrl)
|
||||||
|
return userDetails.profileImageUrl;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageUrl = getImageUrl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.profileAvatarEditContainer}
|
||||||
|
onClick={handleChangeProfileAvatar}
|
||||||
|
>
|
||||||
|
{imageUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={userDetails?.displayName}
|
||||||
|
src={imageUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={96} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.profileAvatarEditOverlay}>
|
||||||
|
<DeviceCameraIcon size={38} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
{...register("displayName")}
|
||||||
|
label={t("display_name")}
|
||||||
|
minLength={3}
|
||||||
|
maxLength={50}
|
||||||
|
containerProps={{ style: { width: "100%" } }}
|
||||||
|
error={errors.displayName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<small style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
|
||||||
|
<Trans i18nKey="privacy_hint" ns="user_profile">
|
||||||
|
<Link to="/settings" />
|
||||||
|
</Trans>
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
style={{ alignSelf: "end", marginTop: `${SPACING_UNIT * 3}px` }}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isSubmitting ? t("saving") : t("save")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -11,8 +11,8 @@ export function LockedProfile() {
|
||||||
<div className={styles.lockIcon}>
|
<div className={styles.lockIcon}>
|
||||||
<LockIcon size={24} />
|
<LockIcon size={24} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{t("locked_profile")}</h2>
|
<h2>{t("locked_profile")}</h2>
|
||||||
<p>{t("locked_profile_description")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import { useCallback, useContext, useEffect, useMemo } from "react";
|
import { useCallback, useContext, useEffect, useMemo } from "react";
|
||||||
import { ProfileHero } from "../profile-hero/profile-hero";
|
import { ProfileHero } from "../profile-hero/profile-hero";
|
||||||
import { useAppDispatch } from "@renderer/hooks";
|
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
@ -17,11 +17,11 @@ import { useNavigate } from "react-router-dom";
|
||||||
import { LockedProfile } from "./locked-profile";
|
import { LockedProfile } from "./locked-profile";
|
||||||
|
|
||||||
export function ProfileContent() {
|
export function ProfileContent() {
|
||||||
const { userProfile } = useContext(userProfileContext);
|
const { userProfile, isMe } = useContext(userProfileContext);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { i18n, t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userProfile) {
|
if (userProfile) {
|
||||||
|
@ -34,11 +34,7 @@ export function ProfileContent() {
|
||||||
return userProfile?.libraryGames.slice(0, 12);
|
return userProfile?.libraryGames.slice(0, 12);
|
||||||
}, [userProfile]);
|
}, [userProfile]);
|
||||||
|
|
||||||
const numberFormatter = useMemo(() => {
|
const { numberFormatter } = useFormat();
|
||||||
return new Intl.NumberFormat(i18n.language, {
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
});
|
|
||||||
}, [i18n.language]);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
@ -65,10 +61,18 @@ export function ProfileContent() {
|
||||||
objectID: game.objectId,
|
objectID: game.objectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const usersAreFriends = useMemo(() => {
|
||||||
|
return userProfile?.relation?.status === "ACCEPTED";
|
||||||
|
}, [userProfile]);
|
||||||
|
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (!userProfile) return null;
|
if (!userProfile) return null;
|
||||||
|
|
||||||
if (userProfile?.profileVisibility === "FRIENDS") {
|
const shouldLockProfile =
|
||||||
|
userProfile.profileVisibility === "PRIVATE" ||
|
||||||
|
(userProfile.profileVisibility === "FRIENDS" && !usersAreFriends);
|
||||||
|
|
||||||
|
if (!isMe && shouldLockProfile) {
|
||||||
return <LockedProfile />;
|
return <LockedProfile />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,6 +217,8 @@ export function ProfileContent() {
|
||||||
numberFormatter,
|
numberFormatter,
|
||||||
t,
|
t,
|
||||||
truncatedGamesList,
|
truncatedGamesList,
|
||||||
|
usersAreFriends,
|
||||||
|
isMe,
|
||||||
navigate,
|
navigate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ export const profileAvatarButton = style({
|
||||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
transition: "all ease 0.3s",
|
transition: "all ease 0.3s",
|
||||||
|
color: vars.color.muted,
|
||||||
":hover": {
|
":hover": {
|
||||||
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.7)",
|
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.7)",
|
||||||
},
|
},
|
||||||
|
@ -69,3 +70,16 @@ export const userInformation = style({
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const currentGameWrapper = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const currentGameDetails = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
});
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { addSeconds } from "date-fns";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import type { FriendRequestAction } from "@types";
|
import type { FriendRequestAction } from "@types";
|
||||||
import { UserProfileSettingsModal } from "../user-profile-settings-modal";
|
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||||
|
|
||||||
type FriendAction =
|
type FriendAction =
|
||||||
| FriendRequestAction
|
| FriendRequestAction
|
||||||
|
@ -26,6 +26,7 @@ type FriendAction =
|
||||||
|
|
||||||
export function ProfileHero() {
|
export function ProfileHero() {
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
|
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||||
|
|
||||||
const context = useContext(userProfileContext);
|
const context = useContext(userProfileContext);
|
||||||
const {
|
const {
|
||||||
|
@ -49,14 +50,22 @@ export function ProfileHero() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleSignOut = useCallback(async () => {
|
const handleSignOut = useCallback(async () => {
|
||||||
|
setIsPerformingAction(true);
|
||||||
|
|
||||||
|
try {
|
||||||
await signOut();
|
await signOut();
|
||||||
|
|
||||||
showSuccessToast(t("successfully_signed_out"));
|
showSuccessToast(t("successfully_signed_out"));
|
||||||
|
} finally {
|
||||||
|
setIsPerformingAction(false);
|
||||||
|
}
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}, [navigate, signOut, showSuccessToast, t]);
|
}, [navigate, signOut, showSuccessToast, t]);
|
||||||
|
|
||||||
const handleFriendAction = useCallback(
|
const handleFriendAction = useCallback(
|
||||||
(userId: string, action: FriendAction) => {
|
(userId: string, action: FriendAction) => {
|
||||||
|
setIsPerformingAction(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (action === "UNDO_FRIENDSHIP") {
|
if (action === "UNDO_FRIENDSHIP") {
|
||||||
undoFriendship(userId).then(getUserProfile);
|
undoFriendship(userId).then(getUserProfile);
|
||||||
|
@ -80,6 +89,8 @@ export function ProfileHero() {
|
||||||
updateFriendRequestState(userId, action).then(getUserProfile);
|
updateFriendRequestState(userId, action).then(getUserProfile);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showErrorToast(t("try_again"));
|
showErrorToast(t("try_again"));
|
||||||
|
} finally {
|
||||||
|
setIsPerformingAction(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
@ -100,12 +111,20 @@ export function ProfileHero() {
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button theme="outline" onClick={() => setShowEditProfileModal(true)}>
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={() => setShowEditProfileModal(true)}
|
||||||
|
disabled={isPerformingAction}
|
||||||
|
>
|
||||||
<PencilIcon />
|
<PencilIcon />
|
||||||
{t("edit_profile")}
|
{t("edit_profile")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button theme="danger" onClick={handleSignOut}>
|
<Button
|
||||||
|
theme="danger"
|
||||||
|
onClick={handleSignOut}
|
||||||
|
disabled={isPerformingAction}
|
||||||
|
>
|
||||||
<SignOutIcon />
|
<SignOutIcon />
|
||||||
{t("sign_out")}
|
{t("sign_out")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -119,6 +138,7 @@ export function ProfileHero() {
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||||
|
disabled={isPerformingAction}
|
||||||
>
|
>
|
||||||
{t("add_friend")}
|
{t("add_friend")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -126,6 +146,7 @@ export function ProfileHero() {
|
||||||
<Button
|
<Button
|
||||||
theme="danger"
|
theme="danger"
|
||||||
onClick={() => handleFriendAction(userProfile.id, "BLOCK")}
|
onClick={() => handleFriendAction(userProfile.id, "BLOCK")}
|
||||||
|
disabled={isPerformingAction}
|
||||||
>
|
>
|
||||||
{t("block_user")}
|
{t("block_user")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -138,6 +159,7 @@ export function ProfileHero() {
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() => handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")}
|
onClick={() => handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")}
|
||||||
|
disabled={isPerformingAction}
|
||||||
>
|
>
|
||||||
<XCircleFillIcon />
|
<XCircleFillIcon />
|
||||||
{t("undo_friendship")}
|
{t("undo_friendship")}
|
||||||
|
@ -152,6 +174,7 @@ export function ProfileHero() {
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||||
}
|
}
|
||||||
|
disabled={isPerformingAction}
|
||||||
>
|
>
|
||||||
<XCircleFillIcon /> {t("cancel_request")}
|
<XCircleFillIcon /> {t("cancel_request")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -165,6 +188,7 @@ export function ProfileHero() {
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||||
}
|
}
|
||||||
|
disabled={isPerformingAction}
|
||||||
>
|
>
|
||||||
<CheckCircleFillIcon /> {t("accept_request")}
|
<CheckCircleFillIcon /> {t("accept_request")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -173,12 +197,20 @@ export function ProfileHero() {
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
||||||
}
|
}
|
||||||
|
disabled={isPerformingAction}
|
||||||
>
|
>
|
||||||
<XCircleFillIcon /> {t("ignore_request")}
|
<XCircleFillIcon /> {t("ignore_request")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [handleFriendAction, handleSignOut, isMe, t, userProfile]);
|
}, [
|
||||||
|
handleFriendAction,
|
||||||
|
handleSignOut,
|
||||||
|
isMe,
|
||||||
|
t,
|
||||||
|
isPerformingAction,
|
||||||
|
userProfile,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleAvatarClick = useCallback(() => {
|
const handleAvatarClick = useCallback(() => {
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
|
@ -196,10 +228,8 @@ export function ProfileHero() {
|
||||||
cancelButtonLabel={t("cancel")}
|
cancelButtonLabel={t("cancel")}
|
||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
<UserProfileSettingsModal
|
<EditProfileModal
|
||||||
visible={showEditProfileModal}
|
visible={showEditProfileModal}
|
||||||
userProfile={userProfile}
|
|
||||||
updateUserProfile={getUserProfile}
|
|
||||||
onClose={() => setShowEditProfileModal(false)}
|
onClose={() => setShowEditProfileModal(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -230,21 +260,8 @@ export function ProfileHero() {
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{currentGame && (
|
{currentGame && (
|
||||||
<div
|
<div className={styles.currentGameWrapper}>
|
||||||
style={{
|
<div className={styles.currentGameDetails}>
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT / 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
to={buildGameDetailsPath({
|
to={buildGameDetailsPath({
|
||||||
...currentGame,
|
...currentGame,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT } from "../../theme.css";
|
||||||
import { globalStyle, style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
export const wrapper = style({
|
export const wrapper = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -7,47 +7,3 @@ export const wrapper = style({
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Legacy styles */
|
|
||||||
export const profileAvatarEditContainer = style({
|
|
||||||
alignSelf: "center",
|
|
||||||
width: "128px",
|
|
||||||
height: "128px",
|
|
||||||
display: "flex",
|
|
||||||
borderRadius: "4px",
|
|
||||||
color: vars.color.body,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
position: "relative",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
|
||||||
cursor: "pointer",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileAvatar = style({
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
objectFit: "cover",
|
|
||||||
borderRadius: "4px",
|
|
||||||
overflow: "hidden",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileAvatarEditOverlay = style({
|
|
||||||
position: "absolute",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
||||||
color: vars.color.muted,
|
|
||||||
zIndex: "1",
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
alignItems: "center",
|
|
||||||
opacity: "0",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle(`${profileAvatarEditContainer}:hover ${profileAvatarEditOverlay}`, {
|
|
||||||
opacity: "1",
|
|
||||||
});
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./user-profile-settings-modal";
|
|
|
@ -1,118 +0,0 @@
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
import { UserFriend } from "@types";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { UserFriendItem } from "@renderer/pages/shared-modals/user-friend-modal/user-friend-item";
|
|
||||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
|
||||||
|
|
||||||
const pageSize = 12;
|
|
||||||
|
|
||||||
export const UserEditProfileBlockList = () => {
|
|
||||||
const { t } = useTranslation("user_profile");
|
|
||||||
const { showErrorToast } = useToast();
|
|
||||||
|
|
||||||
const [page, setPage] = useState(0);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [maxPage, setMaxPage] = useState(0);
|
|
||||||
const [blocks, setBlocks] = useState<UserFriend[]>([]);
|
|
||||||
const listContainer = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const { unblockUser } = useUserDetails();
|
|
||||||
|
|
||||||
const loadNextPage = () => {
|
|
||||||
if (page > maxPage) return;
|
|
||||||
setIsLoading(true);
|
|
||||||
window.electron
|
|
||||||
.getUserBlocks(pageSize, page * pageSize)
|
|
||||||
.then((newPage) => {
|
|
||||||
if (page === 0) {
|
|
||||||
setMaxPage(newPage.totalBlocks / pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
setBlocks([...blocks, ...newPage.blocks]);
|
|
||||||
setPage(page + 1);
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => setIsLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
const scrollTop = listContainer.current?.scrollTop || 0;
|
|
||||||
const scrollHeight = listContainer.current?.scrollHeight || 0;
|
|
||||||
const clientHeight = listContainer.current?.clientHeight || 0;
|
|
||||||
const maxScrollTop = scrollHeight - clientHeight;
|
|
||||||
|
|
||||||
if (scrollTop < maxScrollTop * 0.9 || isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const container = listContainer.current;
|
|
||||||
container?.addEventListener("scroll", handleScroll);
|
|
||||||
return () => container?.removeEventListener("scroll", handleScroll);
|
|
||||||
}, [isLoading]);
|
|
||||||
|
|
||||||
const reloadList = () => {
|
|
||||||
setPage(0);
|
|
||||||
setMaxPage(0);
|
|
||||||
setBlocks([]);
|
|
||||||
loadNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reloadList();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleUnblock = (userId: string) => {
|
|
||||||
unblockUser(userId)
|
|
||||||
.then(() => {
|
|
||||||
reloadList();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
showErrorToast(t("try_again"));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
|
||||||
<div
|
|
||||||
ref={listContainer}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
maxHeight: "400px",
|
|
||||||
overflowY: "scroll",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!isLoading && blocks.length === 0 && <p>{t("no_blocked_users")}</p>}
|
|
||||||
{blocks.map((friend) => {
|
|
||||||
return (
|
|
||||||
<UserFriendItem
|
|
||||||
userId={friend.id}
|
|
||||||
displayName={friend.displayName}
|
|
||||||
profileImageUrl={friend.profileImageUrl}
|
|
||||||
onClickUnblock={handleUnblock}
|
|
||||||
type={"BLOCKED"}
|
|
||||||
key={friend.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isLoading && (
|
|
||||||
<Skeleton
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "54px",
|
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: "4px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SkeletonTheme>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,151 +0,0 @@
|
||||||
import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
|
|
||||||
import { Button, SelectField, TextField } from "@renderer/components";
|
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
|
||||||
import { UserProfile } from "@types";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import * as styles from "../profile.css";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
export interface UserEditProfileProps {
|
|
||||||
userProfile: UserProfile;
|
|
||||||
updateUserProfile: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserEditProfile = ({
|
|
||||||
userProfile,
|
|
||||||
updateUserProfile,
|
|
||||||
}: UserEditProfileProps) => {
|
|
||||||
const { t } = useTranslation("user_profile");
|
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
|
||||||
displayName: userProfile.displayName,
|
|
||||||
profileVisibility: userProfile.profileVisibility,
|
|
||||||
profileImageUrl: null as string | null,
|
|
||||||
});
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
|
||||||
const { patchUser } = useUserDetails();
|
|
||||||
|
|
||||||
const { showSuccessToast, showErrorToast } = useToast();
|
|
||||||
|
|
||||||
const [profileVisibilityOptions, setProfileVisibilityOptions] = useState<
|
|
||||||
{ value: string; label: string }[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setProfileVisibilityOptions([
|
|
||||||
{ value: "PUBLIC", label: t("public") },
|
|
||||||
{ value: "FRIENDS", label: t("friends_only") },
|
|
||||||
{ value: "PRIVATE", label: t("private") },
|
|
||||||
]);
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
const handleChangeProfileAvatar = async () => {
|
|
||||||
const { filePaths } = await window.electron.showOpenDialog({
|
|
||||||
properties: ["openFile"],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "Image",
|
|
||||||
extensions: ["jpg", "jpeg", "png"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (filePaths && filePaths.length > 0) {
|
|
||||||
const path = filePaths[0];
|
|
||||||
|
|
||||||
setForm({ ...form, profileImageUrl: path });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProfileVisibilityChange = (event) => {
|
|
||||||
setForm({
|
|
||||||
...form,
|
|
||||||
profileVisibility: event.target.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveProfile: React.FormEventHandler<HTMLFormElement> = async (
|
|
||||||
event
|
|
||||||
) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsSaving(true);
|
|
||||||
|
|
||||||
patchUser(form)
|
|
||||||
.then(async () => {
|
|
||||||
await updateUserProfile();
|
|
||||||
showSuccessToast(t("saved_successfully"));
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
showErrorToast(t("try_again"));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsSaving(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const avatarUrl = useMemo(() => {
|
|
||||||
if (form.profileImageUrl) return `local:${form.profileImageUrl}`;
|
|
||||||
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
|
||||||
return null;
|
|
||||||
}, [form, userProfile]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={handleSaveProfile}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
width: "350px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.profileAvatarEditContainer}
|
|
||||||
onClick={handleChangeProfileAvatar}
|
|
||||||
>
|
|
||||||
{avatarUrl ? (
|
|
||||||
<img
|
|
||||||
className={styles.profileAvatar}
|
|
||||||
alt={userProfile.displayName}
|
|
||||||
src={avatarUrl}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PersonIcon size={96} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={styles.profileAvatarEditOverlay}>
|
|
||||||
<DeviceCameraIcon size={38} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label={t("display_name")}
|
|
||||||
value={form.displayName}
|
|
||||||
required
|
|
||||||
minLength={3}
|
|
||||||
maxLength={50}
|
|
||||||
containerProps={{ style: { width: "100%" } }}
|
|
||||||
onChange={(e) => setForm({ ...form, displayName: e.target.value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectField
|
|
||||||
label={t("privacy")}
|
|
||||||
value={form.profileVisibility}
|
|
||||||
onChange={handleProfileVisibilityChange}
|
|
||||||
options={profileVisibilityOptions.map((visiblity) => ({
|
|
||||||
key: visiblity.value,
|
|
||||||
value: visiblity.value,
|
|
||||||
label: visiblity.label,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button disabled={isSaving} style={{ alignSelf: "end" }} type="submit">
|
|
||||||
{isSaving ? t("saving") : t("save")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,78 +0,0 @@
|
||||||
import { Button, Modal } from "@renderer/components";
|
|
||||||
import { UserProfile } from "@types";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { UserEditProfile } from "./user-edit-profile";
|
|
||||||
import { UserEditProfileBlockList } from "./user-block-list";
|
|
||||||
|
|
||||||
export interface UserProfileSettingsModalProps {
|
|
||||||
userProfile: UserProfile;
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
updateUserProfile: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserProfileSettingsModal = ({
|
|
||||||
userProfile,
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
updateUserProfile,
|
|
||||||
}: UserProfileSettingsModalProps) => {
|
|
||||||
const { t } = useTranslation("user_profile");
|
|
||||||
|
|
||||||
const tabs = [t("edit_profile"), t("blocked_users")];
|
|
||||||
|
|
||||||
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
|
||||||
|
|
||||||
const renderTab = () => {
|
|
||||||
if (currentTabIndex == 0) {
|
|
||||||
return (
|
|
||||||
<UserEditProfile
|
|
||||||
userProfile={userProfile}
|
|
||||||
updateUserProfile={updateUserProfile}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTabIndex == 1) {
|
|
||||||
return <UserEditProfileBlockList />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
title={t("settings")}
|
|
||||||
onClose={onClose}
|
|
||||||
clickOutsideToClose={false}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
|
||||||
{tabs.map((tab, index) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={tab}
|
|
||||||
theme={index === currentTabIndex ? "primary" : "outline"}
|
|
||||||
onClick={() => setCurrentTabIndex(index)}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
{renderTab()}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
9
src/renderer/src/pages/settings/settings-privacy.css.ts
Normal file
9
src/renderer/src/pages/settings/settings-privacy.css.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
import { SPACING_UNIT } from "../../theme.css";
|
||||||
|
|
||||||
|
export const form = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
77
src/renderer/src/pages/settings/settings-privacy.tsx
Normal file
77
src/renderer/src/pages/settings/settings-privacy.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { Button, SelectField } from "@renderer/components";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import * as styles from "./settings-privacy.css";
|
||||||
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsPrivacy() {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
setValue,
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm<FormValues>();
|
||||||
|
|
||||||
|
const { patchUser, userDetails } = useUserDetails();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userDetails?.profileVisibility) {
|
||||||
|
setValue("profileVisibility", userDetails.profileVisibility);
|
||||||
|
}
|
||||||
|
}, [userDetails, setValue]);
|
||||||
|
|
||||||
|
const visibilityOptions = [
|
||||||
|
{ value: "PUBLIC", label: t("public") },
|
||||||
|
{ value: "FRIENDS", label: t("friends_only") },
|
||||||
|
{ value: "PRIVATE", label: t("private") },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onSubmit = async (values: FormValues) => {
|
||||||
|
await patchUser(values);
|
||||||
|
showSuccessToast(t("changes_saved"));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="profileVisibility"
|
||||||
|
render={({ field }) => (
|
||||||
|
<>
|
||||||
|
<SelectField
|
||||||
|
label={t("profile_visibility")}
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
options={visibilityOptions.map((visiblity) => ({
|
||||||
|
key: visiblity.value,
|
||||||
|
value: visiblity.value,
|
||||||
|
label: visiblity.label,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<small>{t("profile_visibility_description")}</small>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
style={{ alignSelf: "flex-end", marginTop: `${SPACING_UNIT * 2}px` }}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{t("save_changes")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import {
|
||||||
SettingsContextConsumer,
|
SettingsContextConsumer,
|
||||||
SettingsContextProvider,
|
SettingsContextProvider,
|
||||||
} from "@renderer/context";
|
} from "@renderer/context";
|
||||||
|
import { SettingsPrivacy } from "./settings-privacy";
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
|
@ -20,6 +21,7 @@ export function Settings() {
|
||||||
t("behavior"),
|
t("behavior"),
|
||||||
t("download_sources"),
|
t("download_sources"),
|
||||||
"Real-Debrid",
|
"Real-Debrid",
|
||||||
|
t("privacy"),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -39,7 +41,11 @@ export function Settings() {
|
||||||
return <SettingsDownloadSources />;
|
return <SettingsDownloadSources />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentCategoryIndex === 3) {
|
||||||
return <SettingsRealDebrid />;
|
return <SettingsRealDebrid />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SettingsPrivacy />;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
25
src/shared/constants.ts
Normal file
25
src/shared/constants.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
export enum Downloader {
|
||||||
|
RealDebrid,
|
||||||
|
Torrent,
|
||||||
|
Gofile,
|
||||||
|
PixelDrain,
|
||||||
|
Qiwi,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DownloadSourceStatus {
|
||||||
|
UpToDate,
|
||||||
|
Errored,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CatalogueCategory {
|
||||||
|
Hot = "hot",
|
||||||
|
Weekly = "weekly",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SteamContentDescriptor {
|
||||||
|
SomeNudityOrSexualContent = 1,
|
||||||
|
FrequenceViolenceOrGore = 2,
|
||||||
|
AdultOnlySexualContent = 3,
|
||||||
|
FrequentNudityOrSexualContent = 4,
|
||||||
|
GeneralMatureContent = 5,
|
||||||
|
}
|
|
@ -1,20 +1,6 @@
|
||||||
export enum Downloader {
|
import { Downloader } from "./constants";
|
||||||
RealDebrid,
|
|
||||||
Torrent,
|
|
||||||
Gofile,
|
|
||||||
PixelDrain,
|
|
||||||
Qiwi,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DownloadSourceStatus {
|
export * from "./constants";
|
||||||
UpToDate,
|
|
||||||
Errored,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CatalogueCategory {
|
|
||||||
Hot = "hot",
|
|
||||||
Weekly = "weekly",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserNotLoggedInError extends Error {
|
export class UserNotLoggedInError extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { DownloadSourceStatus, Downloader } from "@shared";
|
import type { DownloadSourceStatus, Downloader } from "@shared";
|
||||||
|
import type { SteamAppDetails } from "./steam.types";
|
||||||
|
|
||||||
export type GameStatus =
|
export type GameStatus =
|
||||||
| "active"
|
| "active"
|
||||||
|
@ -12,58 +13,6 @@ export type GameShop = "steam" | "epic";
|
||||||
|
|
||||||
export type FriendRequestAction = "ACCEPTED" | "REFUSED" | "CANCEL";
|
export type FriendRequestAction = "ACCEPTED" | "REFUSED" | "CANCEL";
|
||||||
|
|
||||||
export interface SteamGenre {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SteamScreenshot {
|
|
||||||
id: number;
|
|
||||||
path_thumbnail: string;
|
|
||||||
path_full: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SteamVideoSource {
|
|
||||||
max: string;
|
|
||||||
"480": string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SteamMovies {
|
|
||||||
id: number;
|
|
||||||
mp4: SteamVideoSource;
|
|
||||||
webm: SteamVideoSource;
|
|
||||||
thumbnail: string;
|
|
||||||
name: string;
|
|
||||||
highlight: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SteamAppDetails {
|
|
||||||
name: string;
|
|
||||||
detailed_description: string;
|
|
||||||
about_the_game: string;
|
|
||||||
short_description: string;
|
|
||||||
publishers: string[];
|
|
||||||
genres: SteamGenre[];
|
|
||||||
movies?: SteamMovies[];
|
|
||||||
screenshots?: SteamScreenshot[];
|
|
||||||
pc_requirements: {
|
|
||||||
minimum: string;
|
|
||||||
recommended: string;
|
|
||||||
};
|
|
||||||
mac_requirements: {
|
|
||||||
minimum: string;
|
|
||||||
recommended: string;
|
|
||||||
};
|
|
||||||
linux_requirements: {
|
|
||||||
minimum: string;
|
|
||||||
recommended: string;
|
|
||||||
};
|
|
||||||
release_date: {
|
|
||||||
coming_soon: boolean;
|
|
||||||
date: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameRepack {
|
export interface GameRepack {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -203,73 +152,6 @@ export interface StartGameDownloadPayload {
|
||||||
downloader: Downloader;
|
downloader: Downloader;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RealDebridUnrestrictLink {
|
|
||||||
id: string;
|
|
||||||
filename: string;
|
|
||||||
mimeType: string;
|
|
||||||
filesize: number;
|
|
||||||
link: string;
|
|
||||||
host: string;
|
|
||||||
host_icon: string;
|
|
||||||
chunks: number;
|
|
||||||
crc: number;
|
|
||||||
download: string;
|
|
||||||
streamable: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RealDebridAddMagnet {
|
|
||||||
id: string;
|
|
||||||
// URL of the created resource
|
|
||||||
uri: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RealDebridTorrentInfo {
|
|
||||||
id: string;
|
|
||||||
filename: string;
|
|
||||||
original_filename: string;
|
|
||||||
hash: string;
|
|
||||||
bytes: number;
|
|
||||||
original_bytes: number;
|
|
||||||
host: string;
|
|
||||||
split: number;
|
|
||||||
progress: number;
|
|
||||||
status:
|
|
||||||
| "magnet_error"
|
|
||||||
| "magnet_conversion"
|
|
||||||
| "waiting_files_selection"
|
|
||||||
| "queued"
|
|
||||||
| "downloading"
|
|
||||||
| "downloaded"
|
|
||||||
| "error"
|
|
||||||
| "virus"
|
|
||||||
| "compressing"
|
|
||||||
| "uploading"
|
|
||||||
| "dead";
|
|
||||||
added: string;
|
|
||||||
files: {
|
|
||||||
id: number;
|
|
||||||
path: string;
|
|
||||||
bytes: number;
|
|
||||||
selected: number;
|
|
||||||
}[];
|
|
||||||
links: string[];
|
|
||||||
ended: string;
|
|
||||||
speed: number;
|
|
||||||
seeders: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RealDebridUser {
|
|
||||||
id: number;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
points: number;
|
|
||||||
locale: string;
|
|
||||||
avatar: string;
|
|
||||||
type: string;
|
|
||||||
premium: number;
|
|
||||||
expiration: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserFriend {
|
export interface UserFriend {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
@ -351,3 +233,6 @@ export interface TrendingGame {
|
||||||
description: string;
|
description: string;
|
||||||
background: string;
|
background: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from "./steam.types";
|
||||||
|
export * from "./real-debrid.types";
|
||||||
|
|
66
src/types/real-debrid.types.ts
Normal file
66
src/types/real-debrid.types.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
export interface RealDebridUnrestrictLink {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
mimeType: string;
|
||||||
|
filesize: number;
|
||||||
|
link: string;
|
||||||
|
host: string;
|
||||||
|
host_icon: string;
|
||||||
|
chunks: number;
|
||||||
|
crc: number;
|
||||||
|
download: string;
|
||||||
|
streamable: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RealDebridAddMagnet {
|
||||||
|
id: string;
|
||||||
|
// URL of the created resource
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RealDebridTorrentInfo {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
original_filename: string;
|
||||||
|
hash: string;
|
||||||
|
bytes: number;
|
||||||
|
original_bytes: number;
|
||||||
|
host: string;
|
||||||
|
split: number;
|
||||||
|
progress: number;
|
||||||
|
status:
|
||||||
|
| "magnet_error"
|
||||||
|
| "magnet_conversion"
|
||||||
|
| "waiting_files_selection"
|
||||||
|
| "queued"
|
||||||
|
| "downloading"
|
||||||
|
| "downloaded"
|
||||||
|
| "error"
|
||||||
|
| "virus"
|
||||||
|
| "compressing"
|
||||||
|
| "uploading"
|
||||||
|
| "dead";
|
||||||
|
added: string;
|
||||||
|
files: {
|
||||||
|
id: number;
|
||||||
|
path: string;
|
||||||
|
bytes: number;
|
||||||
|
selected: number;
|
||||||
|
}[];
|
||||||
|
links: string[];
|
||||||
|
ended: string;
|
||||||
|
speed: number;
|
||||||
|
seeders: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RealDebridUser {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
points: number;
|
||||||
|
locale: string;
|
||||||
|
avatar: string;
|
||||||
|
type: string;
|
||||||
|
premium: number;
|
||||||
|
expiration: string;
|
||||||
|
}
|
54
src/types/steam.types.ts
Normal file
54
src/types/steam.types.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
export interface SteamGenre {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SteamScreenshot {
|
||||||
|
id: number;
|
||||||
|
path_thumbnail: string;
|
||||||
|
path_full: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SteamVideoSource {
|
||||||
|
max: string;
|
||||||
|
"480": string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SteamMovies {
|
||||||
|
id: number;
|
||||||
|
mp4: SteamVideoSource;
|
||||||
|
webm: SteamVideoSource;
|
||||||
|
thumbnail: string;
|
||||||
|
name: string;
|
||||||
|
highlight: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SteamAppDetails {
|
||||||
|
name: string;
|
||||||
|
detailed_description: string;
|
||||||
|
about_the_game: string;
|
||||||
|
short_description: string;
|
||||||
|
publishers: string[];
|
||||||
|
genres: SteamGenre[];
|
||||||
|
movies?: SteamMovies[];
|
||||||
|
screenshots?: SteamScreenshot[];
|
||||||
|
pc_requirements: {
|
||||||
|
minimum: string;
|
||||||
|
recommended: string;
|
||||||
|
};
|
||||||
|
mac_requirements: {
|
||||||
|
minimum: string;
|
||||||
|
recommended: string;
|
||||||
|
};
|
||||||
|
linux_requirements: {
|
||||||
|
minimum: string;
|
||||||
|
recommended: string;
|
||||||
|
};
|
||||||
|
release_date: {
|
||||||
|
coming_soon: boolean;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
content_descriptors: {
|
||||||
|
ids: number[];
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/locales/index.ts", "src/shared/index.ts"],
|
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/locales/index.ts", "src/shared/**/*", "src/types/**/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"composite": true,
|
"composite": true,
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"src/renderer/src/**/*.tsx",
|
"src/renderer/src/**/*.tsx",
|
||||||
"src/preload/*.d.ts",
|
"src/preload/*.d.ts",
|
||||||
"src/locales/index.ts",
|
"src/locales/index.ts",
|
||||||
"src/shared/index.ts"
|
"src/shared/**/*"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
|
|
48
yarn.lock
48
yarn.lock
|
@ -955,6 +955,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@fontsource/noto-sans/-/noto-sans-5.0.22.tgz#2c5249347ba84fef16e71a58e0ec01b460174093"
|
resolved "https://registry.yarnpkg.com/@fontsource/noto-sans/-/noto-sans-5.0.22.tgz#2c5249347ba84fef16e71a58e0ec01b460174093"
|
||||||
integrity sha512-PwjvKPGFbgpwfKjWZj1zeUvd7ExUW2AqHE9PF9ysAJ2gOuzIHWE6mEVIlchYif7WC2pQhn+g0w6xooCObVi+4A==
|
integrity sha512-PwjvKPGFbgpwfKjWZj1zeUvd7ExUW2AqHE9PF9ysAJ2gOuzIHWE6mEVIlchYif7WC2pQhn+g0w6xooCObVi+4A==
|
||||||
|
|
||||||
|
"@hookform/resolvers@^3.9.0":
|
||||||
|
version "3.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.9.0.tgz#cf540ac21c6c0cd24a40cf53d8e6d64391fb753d"
|
||||||
|
integrity sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.11.14":
|
"@humanwhocodes/config-array@^0.11.14":
|
||||||
version "0.11.14"
|
version "0.11.14"
|
||||||
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz"
|
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz"
|
||||||
|
@ -2879,10 +2884,10 @@ axe-core@=4.7.0:
|
||||||
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz"
|
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz"
|
||||||
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
|
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
|
||||||
|
|
||||||
axios@^1.6.8:
|
axios@^1.7.7:
|
||||||
version "1.6.8"
|
version "1.7.7"
|
||||||
resolved "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz"
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
|
||||||
integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==
|
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects "^1.15.6"
|
follow-redirects "^1.15.6"
|
||||||
form-data "^4.0.0"
|
form-data "^4.0.0"
|
||||||
|
@ -6430,6 +6435,11 @@ prop-types@^15.8.1:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.13.1"
|
react-is "^16.13.1"
|
||||||
|
|
||||||
|
property-expr@^2.0.5:
|
||||||
|
version "2.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8"
|
||||||
|
integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==
|
||||||
|
|
||||||
proxy-from-env@^1.1.0:
|
proxy-from-env@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||||
|
@ -6486,6 +6496,11 @@ react-dom@^18.2.0:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
scheduler "^0.23.2"
|
scheduler "^0.23.2"
|
||||||
|
|
||||||
|
react-hook-form@^7.53.0:
|
||||||
|
version "7.53.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.0.tgz#3cf70951bf41fa95207b34486203ebefbd3a05ab"
|
||||||
|
integrity sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==
|
||||||
|
|
||||||
react-i18next@^14.1.0:
|
react-i18next@^14.1.0:
|
||||||
version "14.1.1"
|
version "14.1.1"
|
||||||
resolved "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz"
|
resolved "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz"
|
||||||
|
@ -7254,6 +7269,11 @@ tildify@2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a"
|
resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a"
|
||||||
integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==
|
integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==
|
||||||
|
|
||||||
|
tiny-case@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03"
|
||||||
|
integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==
|
||||||
|
|
||||||
tiny-typed-emitter@^2.1.0:
|
tiny-typed-emitter@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz"
|
||||||
|
@ -7301,6 +7321,11 @@ token-types@^5.0.1:
|
||||||
"@tokenizer/token" "^0.3.0"
|
"@tokenizer/token" "^0.3.0"
|
||||||
ieee754 "^1.2.1"
|
ieee754 "^1.2.1"
|
||||||
|
|
||||||
|
toposort@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
|
||||||
|
integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
|
||||||
|
|
||||||
tough-cookie@^4.0.0, tough-cookie@^4.1.3:
|
tough-cookie@^4.0.0, tough-cookie@^4.1.3:
|
||||||
version "4.1.4"
|
version "4.1.4"
|
||||||
resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz"
|
resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz"
|
||||||
|
@ -7383,6 +7408,11 @@ type-fest@^0.20.2:
|
||||||
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
|
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
|
||||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||||
|
|
||||||
|
type-fest@^2.19.0:
|
||||||
|
version "2.19.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
|
||||||
|
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
|
||||||
|
|
||||||
typed-array-buffer@^1.0.2:
|
typed-array-buffer@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz"
|
||||||
|
@ -7878,6 +7908,16 @@ yocto-queue@^1.0.0:
|
||||||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz"
|
||||||
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
||||||
|
|
||||||
|
yup@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/yup/-/yup-1.4.0.tgz#898dcd660f9fb97c41f181839d3d65c3ee15a43e"
|
||||||
|
integrity sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==
|
||||||
|
dependencies:
|
||||||
|
property-expr "^2.0.5"
|
||||||
|
tiny-case "^1.0.3"
|
||||||
|
toposort "^2.0.2"
|
||||||
|
type-fest "^2.19.0"
|
||||||
|
|
||||||
zod@^3.23.8:
|
zod@^3.23.8:
|
||||||
version "3.23.8"
|
version "3.23.8"
|
||||||
resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
|
resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
|
||||||
|
|
Loading…
Reference in a new issue