implermented key display
This commit is contained in:
parent
74e1b25fcf
commit
3218253076
14 changed files with 375 additions and 19 deletions
|
@ -3,6 +3,10 @@
|
||||||
|
|
||||||
$main-container-max-width: 992px;
|
$main-container-max-width: 992px;
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.navigation-container,
|
.navigation-container,
|
||||||
main {
|
main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
import '../../css/common/index.scss';
|
|
||||||
import '../common/index';
|
|
||||||
|
|
||||||
import 'datatables.net-bs5/css/dataTables.bootstrap5.css';
|
|
||||||
|
|
||||||
import { Tab } from 'bootstrap';
|
|
||||||
|
|
||||||
async function checkImportFile() {
|
async function checkImportFile() {
|
||||||
const fileInput = document.querySelector<HTMLInputElement>('#import');
|
const fileInput = document.querySelector<HTMLInputElement>('#import');
|
||||||
|
|
||||||
|
@ -136,20 +129,12 @@ async function doImport() {
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
export function init() {
|
||||||
const triggerTabList = document.querySelectorAll('#key-tab button')
|
|
||||||
triggerTabList.forEach(triggerEl => {
|
|
||||||
const tabTrigger = new Tab(triggerEl)
|
|
||||||
|
|
||||||
triggerEl.addEventListener('click', event => {
|
|
||||||
event.preventDefault()
|
|
||||||
tabTrigger.show()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const importButton = document.querySelector('.js--send-import');
|
const importButton = document.querySelector('.js--send-import');
|
||||||
importButton?.addEventListener('click', checkImportFile);
|
importButton?.addEventListener('click', checkImportFile);
|
||||||
|
|
||||||
const doImportButton = document.querySelector('.js--do-import');
|
const doImportButton = document.querySelector('.js--do-import');
|
||||||
doImportButton?.addEventListener('click', doImport);
|
doImportButton?.addEventListener('click', doImport);
|
||||||
})
|
}
|
22
src/js/pages/keys/index.ts
Normal file
22
src/js/pages/keys/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import '../../../css/common/index.scss';
|
||||||
|
import '../../common/index';
|
||||||
|
|
||||||
|
import { Tab } from 'bootstrap';
|
||||||
|
|
||||||
|
import {init as initImport} from "./import";
|
||||||
|
import {init as initTable} from "./table";
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const triggerTabList = document.querySelectorAll('#key-tab button')
|
||||||
|
triggerTabList.forEach(triggerEl => {
|
||||||
|
const tabTrigger = new Tab(triggerEl)
|
||||||
|
|
||||||
|
triggerEl.addEventListener('click', event => {
|
||||||
|
event.preventDefault()
|
||||||
|
tabTrigger.show()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
initImport();
|
||||||
|
initTable();
|
||||||
|
})
|
98
src/js/pages/keys/table.ts
Normal file
98
src/js/pages/keys/table.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import DataTable from "datatables.net-bs5";
|
||||||
|
import 'datatables.net-bs5/css/dataTables.bootstrap5.css';
|
||||||
|
import {Key} from "../types/entities";
|
||||||
|
import {getIconForKeyState, getKeyStateExplanation} from "../types/keyState";
|
||||||
|
|
||||||
|
import {Dropdown} from "bootstrap";
|
||||||
|
|
||||||
|
const TABLE_AJAX_URL = '/api/dt/keys/provider';
|
||||||
|
|
||||||
|
function getKeyDisplay(parent: HTMLElement, keys: Key[]) {
|
||||||
|
const header = document.createElement("h1");
|
||||||
|
header.innerText = 'Keys';
|
||||||
|
header.classList.add('h6');
|
||||||
|
|
||||||
|
const table = document.createElement("table");
|
||||||
|
table.classList.add('table', 'table-striped', 'w-100');
|
||||||
|
const body = table.createTBody();
|
||||||
|
|
||||||
|
for (const {fromWhere, keyState, key, store, store_link} of keys) {
|
||||||
|
const row = body.insertRow();
|
||||||
|
row.classList.add('cursor-pointer');
|
||||||
|
|
||||||
|
const stateCell = row.insertCell();
|
||||||
|
stateCell.classList.add('w-auto', 'align-content-center');
|
||||||
|
stateCell.innerHTML = `<a data-bs-toggle="tooltip" data-bs-title="${getKeyStateExplanation(keyState)}"><i class="fa-solid ${ getIconForKeyState(keyState) }"></i></a>`
|
||||||
|
|
||||||
|
const anchor = stateCell.querySelector('a');
|
||||||
|
if (!anchor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new Dropdown(anchor);
|
||||||
|
|
||||||
|
row.insertCell().textContent = key;
|
||||||
|
row.insertCell().textContent = store === 'external' ? store_link : store;
|
||||||
|
row.insertCell().textContent = fromWhere;
|
||||||
|
|
||||||
|
row.addEventListener('click', () => {
|
||||||
|
console.log('Key options');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.appendChild(header);
|
||||||
|
parent.appendChild(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
const keyTable = document.querySelector<HTMLTableElement>('.key-table');
|
||||||
|
if (!keyTable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = new DataTable(keyTable, {
|
||||||
|
ajax: {
|
||||||
|
url: TABLE_AJAX_URL
|
||||||
|
},
|
||||||
|
processing: true,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
data: 'gamePicture',
|
||||||
|
searchable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'keysAmount',
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'igdbState',
|
||||||
|
searchable: false,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
ordering: false,
|
||||||
|
serverSide: true,
|
||||||
|
order: [ [1, 'asc'] ],
|
||||||
|
createdRow(row: Node, data: any) {
|
||||||
|
const tableRow = <HTMLTableRowElement>row;
|
||||||
|
tableRow.classList.add('cursor-pointer');
|
||||||
|
|
||||||
|
tableRow.addEventListener('click', () => {
|
||||||
|
const rowAPI = table.row(row);
|
||||||
|
if (rowAPI.child.isShown()) {
|
||||||
|
rowAPI.child.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childRow = document.createElement('tr');
|
||||||
|
const cell = childRow.insertCell();
|
||||||
|
cell.colSpan = row.childNodes.length;
|
||||||
|
|
||||||
|
getKeyDisplay(cell, data.keys);
|
||||||
|
|
||||||
|
rowAPI.child(childRow).show();
|
||||||
|
})
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
25
src/js/pages/types/entities.ts
Normal file
25
src/js/pages/types/entities.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import {KeyState} from "./keyState";
|
||||||
|
|
||||||
|
enum Store {
|
||||||
|
STEAM = 'steam',
|
||||||
|
GOG = 'gog',
|
||||||
|
EPICGAMES = 'epicgames',
|
||||||
|
ORIGIN = 'origin',
|
||||||
|
UPLAY = 'uplay',
|
||||||
|
BATTLENET = 'battlenet',
|
||||||
|
EXTERNAL = 'external'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Game = {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Key = {
|
||||||
|
game?: Game,
|
||||||
|
key: string,
|
||||||
|
store: Store,
|
||||||
|
keyState: KeyState,
|
||||||
|
store_link: string|null,
|
||||||
|
fromWhere: string|null,
|
||||||
|
}
|
||||||
|
|
35
src/js/pages/types/keyState.ts
Normal file
35
src/js/pages/types/keyState.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
export enum KeyState {
|
||||||
|
AVAILABLE = 1,
|
||||||
|
UNKNOWN = 0,
|
||||||
|
RESERVED_FOR_GIFT = -1,
|
||||||
|
CLAIMED = -10
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIconForKeyState(keyState: KeyState): string {
|
||||||
|
switch (keyState) {
|
||||||
|
case KeyState.AVAILABLE:
|
||||||
|
return "fa-check text-success";
|
||||||
|
default:
|
||||||
|
case KeyState.UNKNOWN:
|
||||||
|
return 'fa-question text-info';
|
||||||
|
case KeyState.RESERVED_FOR_GIFT:
|
||||||
|
return 'fa-gift text-warning';
|
||||||
|
case KeyState.CLAIMED:
|
||||||
|
return 'fa-x text-danger';
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKeyStateExplanation(keyState: KeyState): string {
|
||||||
|
switch (keyState) {
|
||||||
|
case KeyState.AVAILABLE:
|
||||||
|
return "This key is available";
|
||||||
|
default:
|
||||||
|
case KeyState.UNKNOWN:
|
||||||
|
return 'The state of this key is unknown';
|
||||||
|
case KeyState.RESERVED_FOR_GIFT:
|
||||||
|
return 'This key is reserved for a gift';
|
||||||
|
case KeyState.CLAIMED:
|
||||||
|
return 'This key was claimed';
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,16 @@ final class Game
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $name
|
* @param string $name
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -26,6 +26,8 @@ final class Key
|
||||||
private string|null $storeLink;
|
private string|null $storeLink;
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private string|null $fromWhere;
|
private string|null $fromWhere;
|
||||||
|
#[ORM\Column(type: 'integer', enumType: KeyState::class)]
|
||||||
|
private KeyState $state;
|
||||||
|
|
||||||
public function __construct(Game $game, User $contributedUser, string $key, Store $store, ?string $storeLink, ?string $fromWhere)
|
public function __construct(Game $game, User $contributedUser, string $key, Store $store, ?string $storeLink, ?string $fromWhere)
|
||||||
{
|
{
|
||||||
|
@ -35,5 +37,46 @@ final class Key
|
||||||
$this->store = $store;
|
$this->store = $store;
|
||||||
$this->storeLink = $storeLink;
|
$this->storeLink = $storeLink;
|
||||||
$this->fromWhere = $fromWhere;
|
$this->fromWhere = $fromWhere;
|
||||||
|
$this->state = KeyState::AVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGame(): Game
|
||||||
|
{
|
||||||
|
return $this->game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContributedUser(): User
|
||||||
|
{
|
||||||
|
return $this->contributedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKey(): string
|
||||||
|
{
|
||||||
|
return $this->key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStore(): Store
|
||||||
|
{
|
||||||
|
return $this->store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStoreLink(): ?string
|
||||||
|
{
|
||||||
|
return $this->storeLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFromWhere(): ?string
|
||||||
|
{
|
||||||
|
return $this->fromWhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getState(): KeyState
|
||||||
|
{
|
||||||
|
return $this->state;
|
||||||
}
|
}
|
||||||
}
|
}
|
12
src/php/Entities/Games/KeyState.php
Normal file
12
src/php/Entities/Games/KeyState.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Entities\Games;
|
||||||
|
|
||||||
|
enum KeyState: int
|
||||||
|
{
|
||||||
|
case AVAILABLE = 1;
|
||||||
|
case UNKNOWN = 0;
|
||||||
|
case RESERVED_FOR_GIFT = -1;
|
||||||
|
case CLAIMED = -10;
|
||||||
|
}
|
|
@ -10,5 +10,7 @@ final class DataTablesAPIRoutes
|
||||||
{
|
{
|
||||||
public static function setupRoutes(RouteGroup $group): void {
|
public static function setupRoutes(RouteGroup $group): void {
|
||||||
AccountsEndpoint::applyRoutes($group);
|
AccountsEndpoint::applyRoutes($group);
|
||||||
|
|
||||||
|
$group->get('/keys/provider', ProviderKeysEndpoint::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
98
src/php/Routing/Api/DataTables/ProviderKeysEndpoint.php
Normal file
98
src/php/Routing/Api/DataTables/ProviderKeysEndpoint.php
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace GamesShop\Routing\Api\DataTables;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\Criteria;
|
||||||
|
use Doctrine\Common\Collections\Order;
|
||||||
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use GamesShop\Entities\Account\User;
|
||||||
|
use GamesShop\Entities\Games\Game;
|
||||||
|
use GamesShop\Entities\Games\Key;
|
||||||
|
use GamesShop\Login\LoginHandler;
|
||||||
|
use GamesShop\Login\UserPermission;
|
||||||
|
use Laminas\Diactoros\Response\JsonResponse;
|
||||||
|
use League\Route\Http\Exception\ForbiddenException;
|
||||||
|
use League\Route\Http\Exception\UnauthorizedException;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
final class ProviderKeysEndpoint
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly LoginHandler $loginHandler,
|
||||||
|
private readonly EntityManager $entityManager,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
if (!$this->loginHandler->isLoggedIn()) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->loginHandler->getCurrentUser();
|
||||||
|
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$params = $request->getQueryParams();
|
||||||
|
$draw = $params['draw'];
|
||||||
|
$start = $params['start'];
|
||||||
|
$length = $params['length'];
|
||||||
|
|
||||||
|
$searchValue = $params['search']['value'];
|
||||||
|
|
||||||
|
$repo = $this->entityManager->getRepository(Game::class);
|
||||||
|
$total = $repo->count();
|
||||||
|
|
||||||
|
$criteria = Criteria::create();
|
||||||
|
$criteria->where(Criteria::expr()->contains('name', $searchValue));
|
||||||
|
$criteria->setFirstResult((int)$start);
|
||||||
|
$criteria->setMaxResults((int)$length);
|
||||||
|
$criteria->orderBy([ 'name' => Order::Ascending ]);
|
||||||
|
|
||||||
|
$values = $repo->matching($criteria);
|
||||||
|
$filteredCount = $values->count();
|
||||||
|
|
||||||
|
$entityManager = $this->entityManager;
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'draw' => $draw,
|
||||||
|
'recordsTotal' => $total,
|
||||||
|
'recordsFiltered' => $filteredCount,
|
||||||
|
'data' =>
|
||||||
|
array_filter($values->map(function (Game $game) use ($entityManager, $user) {
|
||||||
|
$criteria = Criteria::create();
|
||||||
|
$criteria->where(Criteria::expr()->eq('game', $game));
|
||||||
|
$criteria->andWhere(Criteria::expr()->eq('contributedUser', $user));
|
||||||
|
$criteria->orderBy(['state' => Order::Ascending]);
|
||||||
|
|
||||||
|
$keys = $entityManager->getRepository(Key::class)->matching($criteria);
|
||||||
|
|
||||||
|
$keysAmount = $keys->count();
|
||||||
|
if ($keysAmount < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'gamePicture' => '',
|
||||||
|
'name' => $game->getName(),
|
||||||
|
'keysAmount' => $keysAmount,
|
||||||
|
'igdbState' => 'yet to be implermented',
|
||||||
|
'keys' => $keys->map(function (Key $key) use ($entityManager) {
|
||||||
|
return [
|
||||||
|
'keyState' => $key->getState()->value,
|
||||||
|
'key' => $key->getKey(),
|
||||||
|
'store' => $key->getStore()->value,
|
||||||
|
'store_link' => $key->getStoreLink(),
|
||||||
|
'from' => $key->getFromWhere(),
|
||||||
|
];
|
||||||
|
})->toArray()
|
||||||
|
];
|
||||||
|
})->toArray())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
5
src/templates/layout/keyTable.php
Normal file
5
src/templates/layout/keyTable.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,15 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use GamesShop\Entities\Games\KeyAttribute;
|
use GamesShop\Entities\Games\KeyAttribute;
|
||||||
|
use League\Plates\Template\Template;
|
||||||
|
|
||||||
|
assert($this instanceof Template);
|
||||||
|
|
||||||
$this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
|
$this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
|
||||||
?>
|
?>
|
||||||
<meta name="key-attributes" content="<?= htmlspecialchars(json_encode(KeyAttribute::casesAsAssociative())) ?>" />
|
<meta name="key-attributes" content="<?= htmlspecialchars(json_encode(KeyAttribute::casesAsAssociative())) ?>" />
|
||||||
|
|
||||||
|
<h1>My Keys</h1>
|
||||||
<ul id="key-tab" class="nav nav-tabs">
|
<ul id="key-tab" class="nav nav-tabs">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#keys-tab-pane" role="tab">
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#keys-tab-pane" role="tab">
|
||||||
|
@ -21,9 +25,20 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane fade show active" id="keys-tab-pane" role="tabpanel">
|
<div class="tab-pane fade show active" id="keys-tab-pane" role="tabpanel">
|
||||||
Key Table
|
<table class="table table-striped key-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>Game Name</td>
|
||||||
|
<td>Amount Keys</td>
|
||||||
|
<td>IGDB State</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="import-tab-pane" role="tabpanel">
|
<div class="tab-pane fade" id="import-tab-pane" role="tabpanel">
|
||||||
|
<h2>Importer</h2>
|
||||||
<label for="formFile" class="form-label mt-3">Insert import file:</label>
|
<label for="formFile" class="form-label mt-3">Insert import file:</label>
|
||||||
|
|
||||||
<div class="mb-3 input-group">
|
<div class="mb-3 input-group">
|
||||||
|
@ -32,6 +47,8 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-none" id="import-info-container">
|
<div class="d-none" id="import-info-container">
|
||||||
|
<h3>Import Details:</h3>
|
||||||
|
|
||||||
<table class="table table-striped w-100" id="import-attribute-table">
|
<table class="table table-striped w-100" id="import-attribute-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -93,7 +93,7 @@ module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
index: JS_FOLDER + "/pages/index",
|
index: JS_FOLDER + "/pages/index",
|
||||||
'admin/accounts': JS_FOLDER + "/pages/admin/accounts",
|
'admin/accounts': JS_FOLDER + "/pages/admin/accounts",
|
||||||
keys: JS_FOLDER + "/pages/keys",
|
keys: JS_FOLDER + "/pages/keys/index",
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: PUBLIC_FOLDER,
|
path: PUBLIC_FOLDER,
|
||||||
|
|
Loading…
Reference in a new issue