diff --git a/src/js/pages/keys/import.ts b/src/js/pages/keys/import.ts index c56d535..a7c90d9 100644 --- a/src/js/pages/keys/import.ts +++ b/src/js/pages/keys/import.ts @@ -1,3 +1,6 @@ +import {getCurrentlySelectedList} from "./userlists"; +import {getTableAPI} from "./table"; + async function checkImportFile() { const fileInput = document.querySelector('#import'); @@ -108,6 +111,9 @@ async function doImport() { formData.set(`columns[${columnIndex}]`, attribute); } + const listId = getCurrentlySelectedList() ?? 0; + formData.set('listid', listId.toString()); + const response = await fetch( `/api/web/keys/import/perform`, { @@ -127,6 +133,8 @@ async function doImport() { container.classList.add('d-none'); fileInput.value = ''; + + getTableAPI().ajax.reload(); } export function init() { diff --git a/src/js/pages/keys/index.ts b/src/js/pages/keys/index.ts index 69d8cca..dd3d6f3 100644 --- a/src/js/pages/keys/index.ts +++ b/src/js/pages/keys/index.ts @@ -5,6 +5,7 @@ import { Tab } from 'bootstrap'; import {init as initImport} from "./import"; import {init as initTable} from "./table"; +import {init as initUserLists} from "./userlists"; document.addEventListener('DOMContentLoaded', () => { const triggerTabList = document.querySelectorAll('#key-tab button') @@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => { initImport(); initTable(); + initUserLists(); }) \ No newline at end of file diff --git a/src/js/pages/keys/table.ts b/src/js/pages/keys/table.ts index 84fd843..e4fd877 100644 --- a/src/js/pages/keys/table.ts +++ b/src/js/pages/keys/table.ts @@ -1,12 +1,19 @@ -import DataTable from "datatables.net-bs5"; +import DataTable, {Api} 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"; +import {Dropdown, Tooltip} from "bootstrap"; +import {getCurrentlySelectedList} from "./userlists"; const TABLE_AJAX_URL = '/api/dt/keys/provider'; +let tableAPI: Api; + +export function getTableAPI(): Api { + return tableAPI; +} + function getKeyDisplay(parent: HTMLElement, keys: Key[]) { const header = document.createElement("h1"); header.innerText = 'Keys'; @@ -16,19 +23,19 @@ function getKeyDisplay(parent: HTMLElement, keys: Key[]) { table.classList.add('table', 'table-striped', 'w-100'); const body = table.createTBody(); - for (const {fromWhere, keyState, key, store, store_link} of keys) { + for (const {fromWhere, state, 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 = `` + stateCell.innerHTML = `` const anchor = stateCell.querySelector('a'); if (!anchor) { return; } - new Dropdown(anchor); + new Tooltip(anchor); row.insertCell().textContent = key; row.insertCell().textContent = store === 'external' ? store_link : store; @@ -49,9 +56,13 @@ export function init() { return; } - const table = new DataTable(keyTable, { + const table = tableAPI = new DataTable(keyTable, { ajax: { - url: TABLE_AJAX_URL + url: TABLE_AJAX_URL, + data: function (d) { + // @ts-ignore + d.listid = getCurrentlySelectedList(); + } }, processing: true, columns: [ @@ -72,7 +83,6 @@ export function init() { } ], ordering: false, - serverSide: true, order: [ [1, 'asc'] ], createdRow(row: Node, data: any) { const tableRow = row; diff --git a/src/js/pages/keys/userlists.ts b/src/js/pages/keys/userlists.ts new file mode 100644 index 0000000..f38b482 --- /dev/null +++ b/src/js/pages/keys/userlists.ts @@ -0,0 +1,71 @@ +import {Modal} from "bootstrap"; +import {getTableAPI} from "./table"; + +export function getCurrentlySelectedList(): number|null { + const listSelect = document.querySelector('#list-select'); + if (!listSelect) { + return null; + } + + return parseInt(listSelect.value); +} + +export function init() { + const modal = document.querySelector('#create-list-modal'); + if (!modal) { + return; + } + + const modalObj = new Modal(modal); + + modal.addEventListener('show.bs.modal', (e) => { + const input = modal.querySelector('#createListName'); + if (!input) { + return; + } + + input.value = ''; + }) + modal.querySelector('.js--create-list')?.addEventListener('click', async (e) => { + const input = modal.querySelector('#createListName'); + if (!input) { + return; + } + + const newName = input.value; + + const formData = new FormData(); + formData.append('name', newName); + + const response = await fetch( + `/api/web/keys/list/create`, + { + method: 'POST', + body: formData + } + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + + window.location.reload(); + }); + + const listSelect = document.querySelector('#list-select'); + if (listSelect) { + listSelect.addEventListener('change', (e) => { + if (listSelect.value === '_create') { + modalObj.show() + return; + } + + getTableAPI().ajax.reload(); + }) + } + + const newButton = document.querySelector('.js--create-list-button'); + if (newButton) { + newButton.addEventListener('click', () => modalObj.show()) + } +} \ No newline at end of file diff --git a/src/js/pages/types/entities.ts b/src/js/pages/types/entities.ts index 2dcde77..626f0c3 100644 --- a/src/js/pages/types/entities.ts +++ b/src/js/pages/types/entities.ts @@ -18,7 +18,7 @@ export type Key = { game?: Game, key: string, store: Store, - keyState: KeyState, + state: KeyState, store_link: string|null, fromWhere: string|null, } diff --git a/src/php/Entities/Games/Game.php b/src/php/Entities/Games/Game.php index 9b3c834..0f7d498 100644 --- a/src/php/Entities/Games/Game.php +++ b/src/php/Entities/Games/Game.php @@ -7,7 +7,7 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[ORM\Table(name: 'games')] -final class Game +class Game { #[ORM\Id] #[ORM\Column(type: 'integer', options: ['unsigned' => true])] diff --git a/src/php/Entities/Games/Key.php b/src/php/Entities/Games/Key.php index 47b0428..67e865f 100644 --- a/src/php/Entities/Games/Key.php +++ b/src/php/Entities/Games/Key.php @@ -5,10 +5,12 @@ namespace GamesShop\Entities\Games; use Doctrine\ORM\Mapping as ORM; use GamesShop\Entities\Account\User; +use GamesShop\Entities\GamesList; +use JsonSerializable; #[ORM\Entity] #[ORM\Table(name: 'keys')] -final class Key +final class Key implements JsonSerializable { #[ORM\Id] #[ORM\Column(type: 'integer', options: ['unsigned' => true])] @@ -17,7 +19,7 @@ final class Key #[ORM\ManyToOne] private Game $game; #[ORM\ManyToOne] - private User $contributedUser; + private GamesList $list; #[ORM\Column] private string $key; #[ORM\Column(type: 'string', enumType: Store::class)] @@ -29,10 +31,10 @@ final class Key #[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, GamesList $list, string $key, Store $store, ?string $storeLink, ?string $fromWhere) { $this->game = $game; - $this->contributedUser = $contributedUser; + $this->list = $list; $this->key = $key; $this->store = $store; $this->storeLink = $storeLink; @@ -79,4 +81,16 @@ final class Key { return $this->state; } + + public function jsonSerialize(): mixed + { + return [ + 'id' => $this->id, + 'key' => $this->key, + 'store' => $this->store->value, + 'store_link' => $this->storeLink, + 'from_where' => $this->fromWhere, + 'state' => $this->state->value, + ]; + } } \ No newline at end of file diff --git a/src/php/Entities/GamesList.php b/src/php/Entities/GamesList.php new file mode 100644 index 0000000..20d4b18 --- /dev/null +++ b/src/php/Entities/GamesList.php @@ -0,0 +1,62 @@ + true])] + #[ORM\GeneratedValue] + private int|null $id; + + #[ORM\ManyToOne] + private User $owner; + + #[ORM\Column(nullable: true)] + private string|null $name; + + #[ORM\JoinTable(name: 'games_list_claimer')] + #[ORM\JoinColumn(name: 'id', referencedColumnName: 'id')] + #[ORM\ManyToMany(targetEntity: User::class)] + private Collection $claimer; + + /** + * @param User $owner + * @param string|null $name + */ + public function __construct(User $owner, ?string $name) + { + $this->owner = $owner; + $this->name = $name; + } + + + public function getId(): ?int + { + return $this->id; + } + + public function getOwner(): User + { + return $this->owner; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getClaimer(): array + { + return $this->claimer; + } + + +} \ No newline at end of file diff --git a/src/php/Importer/GameImporter.php b/src/php/Importer/GameImporter.php index af1d886..34a5e0e 100644 --- a/src/php/Importer/GameImporter.php +++ b/src/php/Importer/GameImporter.php @@ -10,6 +10,7 @@ use GamesShop\Entities\Games\Game; use GamesShop\Entities\Games\Key; use GamesShop\Entities\Games\KeyAttribute; use GamesShop\Entities\Games\Store; +use GamesShop\Entities\GamesList; use PhpOffice\PhpSpreadsheet\IOFactory; final class GameImporter @@ -75,7 +76,7 @@ final class GameImporter /** * @param string[] $columnDefinitions */ - public function import(string $path, array $columnDefinitions, User $contributedUser): array { + public function import(string $path, array $columnDefinitions, GamesList $list): array { $spreadsheet = IOFactory::load($path); $worksheet = $spreadsheet->getSheet(0); @@ -129,7 +130,7 @@ final class GameImporter $key = new Key( $game, - $contributedUser, + $list, $values['key'], $values['store'], $values['store_link'], diff --git a/src/php/Routing/Api/DataTables/ProviderKeysEndpoint.php b/src/php/Routing/Api/DataTables/ProviderKeysEndpoint.php index 8f53324..bd2aefa 100644 --- a/src/php/Routing/Api/DataTables/ProviderKeysEndpoint.php +++ b/src/php/Routing/Api/DataTables/ProviderKeysEndpoint.php @@ -9,9 +9,11 @@ use Doctrine\ORM\EntityManager; use GamesShop\Entities\Account\User; use GamesShop\Entities\Games\Game; use GamesShop\Entities\Games\Key; +use GamesShop\Entities\GamesList; use GamesShop\Login\LoginHandler; use GamesShop\Login\UserPermission; use Laminas\Diactoros\Response\JsonResponse; +use League\Route\Http\Exception\BadRequestException; use League\Route\Http\Exception\ForbiddenException; use League\Route\Http\Exception\UnauthorizedException; use Psr\Http\Message\ResponseInterface; @@ -37,62 +39,40 @@ final class ProviderKeysEndpoint throw new ForbiddenException(); } + $body = $request->getQueryParams(); + if (!array_key_exists('listid', $body)) { + throw new BadRequestException(); + } - $params = $request->getQueryParams(); - $draw = $params['draw']; - $start = $params['start']; - $length = $params['length']; + $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); + if (!$list instanceof GamesList) { + throw new BadRequestException(); + } - $searchValue = $params['search']['value']; + $keys = $this->entityManager->getRepository(Key::class)->findBy(['list' => $list]); + $gameToKeyArray = []; + foreach ($keys as $key) { + $game = $key->getGame(); + $id = $game->getId(); - $repo = $this->entityManager->getRepository(Game::class); - $total = $repo->count(); + if (!array_key_exists($id, $gameToKeyArray)) { + $gameToKeyArray[$id] = [ $game, [] ]; + } - $criteria = Criteria::create(); - $criteria->where(Criteria::expr()->contains('name', $searchValue)); - $criteria->setFirstResult((int)$start); - $criteria->setMaxResults((int)$length); - $criteria->orderBy([ 'name' => Order::Ascending ]); + $gameToKeyArray[$id][1][] = $key; + } - $values = $repo->matching($criteria); - $filteredCount = $values->count(); + $result = []; + foreach ($gameToKeyArray as [$game, $keys]) { + $result[] = [ + 'gamePicture' => '', + 'name' => $game->getName(), + 'keysAmount' => count($keys), + 'igdbState' => 'not implermented', + 'keys' => $keys, + ]; + } - $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()) - ]); + return new JsonResponse([ 'data' => $result ]); } } \ No newline at end of file diff --git a/src/php/Routing/Api/Web/CreateKeyListRoute.php b/src/php/Routing/Api/Web/CreateKeyListRoute.php new file mode 100644 index 0000000..a96c5a4 --- /dev/null +++ b/src/php/Routing/Api/Web/CreateKeyListRoute.php @@ -0,0 +1,49 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + $body = $request->getParsedBody(); + if (!array_key_exists('name', $body)) { + throw new BadRequestException(); + } + + $name = $body['name']; + $list = new GamesList($user, $name); + $this->entityManager->persist($list); + $this->entityManager->flush(); + + return new Response(); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/ImportKeysRoute.php b/src/php/Routing/Api/Web/ImportKeysRoute.php index 17c00ac..be5f98b 100644 --- a/src/php/Routing/Api/Web/ImportKeysRoute.php +++ b/src/php/Routing/Api/Web/ImportKeysRoute.php @@ -3,6 +3,8 @@ declare(strict_types=1); namespace GamesShop\Routing\Api\Web; +use Doctrine\ORM\EntityManager; +use GamesShop\Entities\GamesList; use GamesShop\Importer\GameImporter; use GamesShop\Login\LoginHandler; use GamesShop\Login\UserPermission; @@ -19,6 +21,7 @@ final class ImportKeysRoute public function __construct( private readonly LoginHandler $loginHandler, private readonly GameImporter $importer, + private readonly EntityManager $entityManager, ) { } public function __invoke(ServerRequestInterface $request): ResponseInterface @@ -32,6 +35,16 @@ final class ImportKeysRoute throw new ForbiddenException(); } + $body = $request->getParsedBody(); + if (!array_key_exists('listid', $body)) { + throw new BadRequestException(); + } + + $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); + if (!$list instanceof GamesList) { + throw new BadRequestException(); + } + /** * @var UploadedFile $file */ @@ -45,7 +58,7 @@ final class ImportKeysRoute $columnDefs = $request->getParsedBody()['columns']; - [$total, $imported] = $this->importer->import($fileName, $columnDefs, $user); + [$total, $imported] = $this->importer->import($fileName, $columnDefs, $list); unlink($fileName); return new JsonResponse([ 'success' => true, 'total' => $total, 'imported' => $imported ]); diff --git a/src/php/Routing/Api/Web/WebAPIRoutes.php b/src/php/Routing/Api/Web/WebAPIRoutes.php index 4007296..f64744a 100644 --- a/src/php/Routing/Api/Web/WebAPIRoutes.php +++ b/src/php/Routing/Api/Web/WebAPIRoutes.php @@ -12,5 +12,7 @@ final class WebAPIRoutes $group->post('/keys/import/prepare', ImportKeysPrepareRoute::class); $group->post('/keys/import/perform', ImportKeysRoute::class); + + $group->post('/keys/list/create', CreateKeyListRoute::class); } } \ No newline at end of file diff --git a/src/php/Routing/ErrorRoute.php b/src/php/Routing/ErrorRoute.php index 736461c..a9a71f8 100644 --- a/src/php/Routing/ErrorRoute.php +++ b/src/php/Routing/ErrorRoute.php @@ -13,7 +13,7 @@ final class ErrorRoute public function renderErrorPage(int $errorCode): ResponseInterface { $pageContent = ContainerHandler::get(TemplateEngine::class)->renderErrorPage($errorCode); - $response = new Response; + $response = new Response(status: $errorCode); $response->getBody()->write($pageContent); return $response; } diff --git a/src/php/Routing/KeysRoute.php b/src/php/Routing/KeysRoute.php index 977c095..b66e8d8 100644 --- a/src/php/Routing/KeysRoute.php +++ b/src/php/Routing/KeysRoute.php @@ -3,6 +3,8 @@ declare(strict_types=1); namespace GamesShop\Routing; +use Doctrine\ORM\EntityManager; +use GamesShop\Entities\GamesList; use GamesShop\Login\LoginHandler; use GamesShop\Login\UserPermission; use GamesShop\Routing\Responses\TemplateResponse; @@ -14,7 +16,8 @@ use Psr\Http\Message\ServerRequestInterface; final class KeysRoute { public function __construct( - private readonly LoginHandler $loginHandler + private readonly LoginHandler $loginHandler, + private readonly EntityManager $entityManager ) { } @@ -30,8 +33,9 @@ final class KeysRoute throw new ForbiddenException(); } - - return new TemplateResponse('key-manager'); + $entityManager = $this->entityManager->getRepository(GamesList::class); + $lists = $entityManager->findBy([ 'owner' => $user ]); + return new TemplateResponse('key-manager', [ 'usersLists' => $lists ]); } public static function applyRoutes(\League\Route\Router $router): void diff --git a/src/templates/pages/key-manager.php b/src/templates/pages/key-manager.php index 2108b48..ec3005d 100644 --- a/src/templates/pages/key-manager.php +++ b/src/templates/pages/key-manager.php @@ -2,15 +2,44 @@ declare(strict_types=1); use GamesShop\Entities\Games\KeyAttribute; +use GamesShop\Entities\GamesList; use League\Plates\Template\Template; assert($this instanceof Template); +/** @var GamesList[] $usersLists */ + $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]); ?> -

My Keys

+
+
+

My Keys

+
+
+ + + +
+
+ + +
+

You don't have a key list. Create one here.

+ + +
+ + +
@@ -65,4 +99,45 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
- \ No newline at end of file +
+

+ Share your list +

+ + +
+ + +
+ + +
+ + + + +start('modal') ?> + + + +end() ?>