implermented key display

This commit is contained in:
Michel Fedde 2024-07-06 20:49:50 +02:00
parent 74e1b25fcf
commit 3218253076
14 changed files with 375 additions and 19 deletions

View file

@ -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%;

View file

@ -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);
}) }

View 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();
})

View 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();
})
},
});
}

View 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,
}

View 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';
}
}

View file

@ -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
*/ */

View file

@ -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;
} }
} }

View 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;
}

View file

@ -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);
} }
} }

View 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())
]);
}
}

View file

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
?>

View file

@ -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>

View file

@ -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,