Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/pages/players/index.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
.search-button {
padding: .2rem;
border-radius: 4px;
cursor: pointer;
}

.search-button:disabled {
background-color: var(--color-gold-two);
color: var(--color-gray);
opacity: 0.5;
position: relative;
cursor: not-allowed;
}

.search-button:disabled::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 4px;
pointer-events: none;
}

.search-controls {
Expand Down
91 changes: 62 additions & 29 deletions src/pages/players/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { Link, useSearchParams } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import { Turnstile } from '@marsidev/react-turnstile'
import { Turnstile } from '@marsidev/react-turnstile';
import Select from 'react-select';
import { Icon } from '@mdi/react';
import { mdiAccountSearch } from '@mdi/js';

import LoadingSmall from '../../components/loading-small/index.js';

import useKeyPress from '../../hooks/useKeyPress.jsx';

import SEO from '../../components/SEO.jsx';
Expand All @@ -18,7 +20,7 @@ import gameModes from '../../data/game-modes.json';
import './index.css';

function Players() {
const [ searchParams, setSearchParams ] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const turnstileRef = useRef();
const turnstileToken = useRef(false);

Expand All @@ -30,23 +32,26 @@ function Players() {
const defaultGameMode = useMemo(() => {
return searchParams.get('gameMode') ?? gameModeSetting;
}, [searchParams, gameModeSetting]);
const [ gameMode, setGameMode ] = useState(defaultGameMode);
const lastSearch = useRef({name: '', gameMode});
const [gameMode, setGameMode] = useState(defaultGameMode);
const lastSearch = useRef({ name: '', gameMode });

const [nameFilter, setNameFilter] = useState('');
const [nameResults, setNameResults] = useState([]);
const [nameResultsError, setNameResultsError] = useState(false);

const [isButtonDisabled, setButtonDisabled] = useState(true);
const [searched, setSearched] = useState(false);
const [searching, setSearching] = useState(false);
const [tokenState, setTokenState] = useState(false);

const searchTextValid = useMemo(() => {
const charactersValid = !!nameFilter.match(/^[a-zA-Z0-9-_]*$/);
const lengthValid = !!nameFilter.match(/^[a-zA-Z0-9-_]{3,15}$|^TarkovCitizen\d{1,10}$/i);
setNameResultsError(false);
if (!charactersValid) {
setNameResultsError(`Names can only contain letters, numbers, dashes (-), and underscores (_)`);
setNameResultsError(
`Names can only contain letters, numbers, dashes (-), and underscores (_)`,
);
}
return charactersValid && lengthValid;
}, [nameFilter, setNameResultsError]);
Expand Down Expand Up @@ -75,8 +80,13 @@ function Players() {
return;
}
try {
setSearching(true);
setNameResultsError(false);
setNameResults((await playerStats.searchPlayers(nameFilter, gameMode, turnstileToken.current)).sort((a, b) => a.name.localeCompare(b.name)));
setNameResults(
(
await playerStats.searchPlayers(nameFilter, gameMode, turnstileToken.current)
).sort((a, b) => a.name.localeCompare(b.name)),
);
setSearched(true);
lastSearch.current.name = nameFilter;
lastSearch.current.gameMode = gameMode;
Expand All @@ -85,12 +95,22 @@ function Players() {
lastSearch.current.name = '';
setNameResults([]);
setNameResultsError(error.message);
} finally {
setSearching(false);
}
setTokenState(false);
if (turnstileRef.current?.reset) {
turnstileRef.current.reset();
}
}, [nameFilter, searchTextValid, setNameResults, setNameResultsError, turnstileToken, turnstileRef, gameMode]);
}, [
nameFilter,
searchTextValid,
setNameResults,
setNameResultsError,
turnstileToken,
turnstileRef,
gameMode,
]);

const searchResults = useMemo(() => {
if (!searched) {
Expand All @@ -101,18 +121,18 @@ function Players() {
}
let morePlayers = '';
if (nameResults.length >= 5) {
morePlayers = <p>{t('Refine your search to get better results')}</p>
morePlayers = <p>{t('Refine your search to get better results')}</p>;
}
return (
<div>
{morePlayers}
<ul className="name-results-list">
{nameResults.map(result => {
return <li key={`account-${result.aid}`}>
<Link to={`/players/${gameMode}/${result.aid}`}>
{result.name}
</Link>
</li>
{nameResults.map((result) => {
return (
<li key={`account-${result.aid}`}>
<Link to={`/players/${gameMode}/${result.aid}`}>{result.name}</Link>
</li>
);
})}
</ul>
</div>
Expand All @@ -128,7 +148,10 @@ function Players() {
return [
<SEO
title={`${t('Players')} - ${t('Escape from Tarkov')} - ${t('Tarkov.dev')}`}
description={t('players-page-description', 'Search Escape from Tarkov players. View player profiles and see their stats.')}
description={t(
'players-page-description',
'Search Escape from Tarkov players. View player profiles and see their stats.',
)}
key="seo-wrapper"
/>,
<div className={'page-wrapper'} key="players-page-wrapper">
Expand All @@ -140,35 +163,37 @@ function Players() {
</div>
<div>
<Trans i18nKey={'players-page-p'}>
<p>
Search for Escape From Tarkov players and view their profiles.
</p>
<p>Search for Escape From Tarkov players and view their profiles.</p>
</Trans>
</div>
<label className={'single-filter-wrapper'} style={{marginBottom: '1em'}}>
<label className={'single-filter-wrapper'} style={{ marginBottom: '1em' }}>
<span className={'single-filter-label'}>{t('Game mode')}</span>
<Select
label={t('Game mode')}
placeholder={t(`game_mode_${defaultGameMode}`)}
defaultValue={defaultGameMode}
options={gameModes.map(m => {
options={gameModes.map((m) => {
return {
label: t(`game_mode_${m}`),
value: m,
}
};
})}
className="basic-multi-select game-mode"
classNamePrefix="select"
onChange={(event) => {
setSearchParams({gameMode: event.value});
if (searchTextValid && gameMode !== event.value && !!turnstileToken.current) {
setSearchParams({ gameMode: event.value });
if (
searchTextValid &&
gameMode !== event.value &&
!!turnstileToken.current
) {
setButtonDisabled(false);
}
setGameMode(event.value);
}}
/>
</label>
<div className='search-controls'>
<div className="search-controls">
<InputFilter
label={t('Player Name')}
defaultValue={nameFilter}
Expand All @@ -180,31 +205,39 @@ function Players() {
}}
className="player-name-search"
/>
<button className="search-button" onClick={searchForName} disabled={isButtonDisabled}>{t('Search')}</button>
<button
className="search-button"
onClick={searchForName}
disabled={isButtonDisabled}
>
{searching ? <LoadingSmall /> : t('Search')}
</button>
</div>
{!!nameResultsError && (
<div>
<p className="error">{nameResultsError}</p>
</div>
)}
<Turnstile
<Turnstile
ref={turnstileRef}
className="turnstile-widget"
siteKey='0x4AAAAAAAVVIHGZCr2PPwrR'
siteKey="0x4AAAAAAAVVIHGZCr2PPwrR"
onSuccess={(token) => {
setTokenState(token);
}}
onError={(errorCode) => {
// https://developers.cloudflare.com/turnstile/reference/client-side-errors#error-codes
if (errorCode === '110200') {
setNameResultsError(`Turnstile error: ${window.location.hostname} is not a valid hostname`);
setNameResultsError(
`Turnstile error: ${window.location.hostname} is not a valid hostname`,
);
} else if (errorCode.startsWith('600')) {
setNameResultsError('Turnstile challenge failed');
} else {
setNameResultsError(`Turnstile error code ${errorCode}`);
}
}}
options={{appearance: 'interaction-only'}}
options={{ appearance: 'interaction-only' }}
/>
{!nameResultsError && searchResults}
</div>,
Expand Down