diff --git a/package-lock.json b/package-lock.json index f9e24b3..f315ef4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@fortawesome/fontawesome-free": "^6.5.2", "@types/bootstrap": "^5.2.10", "bootstrap": "^5.3.3", + "bootstrap5-autocomplete": "^1.1.33", "datatables.net-bs5": "^2.0.8" }, "devDependencies": { @@ -699,6 +700,12 @@ "@popperjs/core": "^2.11.8" } }, + "node_modules/bootstrap5-autocomplete": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/bootstrap5-autocomplete/-/bootstrap5-autocomplete-1.1.33.tgz", + "integrity": "sha512-VgHSx2hCNEBThFzb57HziDA2BNuc0wT5+V9XqIbXsV6oKYXcyRE2ytFIJcHjTCEIYqTsNCFiCQILIXc3YANGPQ==", + "license": "MIT" + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", diff --git a/package.json b/package.json index f2e9f45..114a70b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@fortawesome/fontawesome-free": "^6.5.2", "@types/bootstrap": "^5.2.10", "bootstrap": "^5.3.3", + "bootstrap5-autocomplete": "^1.1.33", "datatables.net-bs5": "^2.0.8" }, "devDependencies": { diff --git a/src/js/pages/keys/share.ts b/src/js/pages/keys/share.ts index fad468a..d7a9ef9 100644 --- a/src/js/pages/keys/share.ts +++ b/src/js/pages/keys/share.ts @@ -1,24 +1,130 @@ -import DataTable from "datatables.net-bs5"; +import DataTable, {Api} from "datatables.net-bs5"; +// @ts-expect-error +import Autocomplete from "bootstrap5-autocomplete"; +import {getCurrentlySelectedList} from "./userlists"; + +const SEARCH_API_URL = '/api/web/share/search'; +const ADD_API_URL = '/api/web/share/add'; +const REMOVE_API_URL = '/api/web/share/remove'; + +const DT_API_URL = '/api/dt/list/users'; + +let dtApi: Api; + +function actionColumnRender( + id: number, type: string +) { + if (type !== 'display') { + return id; + } + + const icon = document.createElement('i'); + icon.classList.add('fa-solid', 'fa-xmark', 'text-danger', 'cursor-pointer', 'fa-xl'); + icon.addEventListener('click', async () => { + const formData = new FormData(); + formData.append('requestedUser', id.toString() ?? ''); + formData.append('listid', getCurrentlySelectedList()?.toString() ?? ''); + + const response = await fetch( + REMOVE_API_URL, + { + method: 'POST', + body: formData + } + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + + dtApi.ajax.reload(); + }) + + return icon; +} export function initShare() { + const input = document.querySelector('#share-user-search'); + if (!input) { + throw new Error("Missing search element"); + } + let currentlySelected: number|null = null; + const autocomplete = new Autocomplete(input, { + server: SEARCH_API_URL, + liveServer: true, + fullWidth: true, + fixed: true, + + onSelectItem(item: Object, instance: Autocomplete) { + // @ts-expect-error + currentlySelected = item.value; + } + }) + + document.querySelector('.js--adds-user')?.addEventListener('click', async (e) => { + const formData = new FormData(); + formData.append('requested', currentlySelected?.toString() ?? ''); + formData.append('listid', getCurrentlySelectedList()?.toString() ?? ''); + + const response = await fetch( + ADD_API_URL, + { + method: 'POST', + body: formData + } + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + + dtApi.ajax.reload() + }) + const table = document.querySelector('#shared-users-table'); if (!table) { return; } - new DataTable(table, { + dtApi = new DataTable(table, { columns: [ { name: '', + title: 'Icon', data: 'icon', + className: 'avatar', searchable: false, orderable: false, + width: 'auto', + render(data, type) { + if (type !== 'display') { + return data; + } + + return `Profile Picture`; + } }, { name: 'Name', data: 'name', + title: 'Name' + }, + { + name: 'actions', + data: 'id', + render: actionColumnRender, + searchable: false, + orderable: false, + type: 'html' } ], + ajax: { + url: DT_API_URL, + data(data) { + // @ts-expect-error + data.listid = getCurrentlySelectedList() + } + }, order: [ [1, "desc"] ] }); } \ No newline at end of file diff --git a/src/php/Entities/GamesList.php b/src/php/Entities/GamesList.php index 6872628..647d207 100644 --- a/src/php/Entities/GamesList.php +++ b/src/php/Entities/GamesList.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace GamesShop\Entities; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use GamesShop\Entities\Account\User; @@ -35,6 +36,7 @@ final class GamesList { $this->owner = $owner; $this->name = $name; + $this->claimer = new ArrayCollection([$owner]); } @@ -53,10 +55,13 @@ final class GamesList return $this->name; } - public function getClaimer(): array + public function getClaimer(): Collection { return $this->claimer; } - - + + public function addClaimer(User $claimer): void + { + $this->claimer[] = $claimer; + } } \ No newline at end of file diff --git a/src/php/Importer/GameImporter.php b/src/php/Importer/GameImporter.php index c579a21..b686d12 100644 --- a/src/php/Importer/GameImporter.php +++ b/src/php/Importer/GameImporter.php @@ -119,7 +119,7 @@ final class GameImporter } } - if ($values['key'] === null || $values['name'] === null || $values['store'] === null) { + if (empty($values['key']) || empty($values['name']) || empty($values['store'])) { continue; } diff --git a/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php b/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php index 3d6a860..ef2a964 100644 --- a/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php +++ b/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php @@ -12,5 +12,6 @@ final class DataTablesAPIRoutes AccountsEndpoint::applyRoutes($group); $group->get('/keys/provider', ProviderKeysEndpoint::class); + $group->get('/list/users', SharedUsersEndpoint::class); } } \ No newline at end of file diff --git a/src/php/Routing/Api/DataTables/SharedUsersEndpoint.php b/src/php/Routing/Api/DataTables/SharedUsersEndpoint.php new file mode 100644 index 0000000..6e2314b --- /dev/null +++ b/src/php/Routing/Api/DataTables/SharedUsersEndpoint.php @@ -0,0 +1,63 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + $body = $request->getQueryParams(); + if (!array_key_exists('listid', $body)) { + throw new BadRequestException(); + } + + $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); + $claimer = $list->getClaimer(); + + return new JsonResponse( + [ 'data' => $claimer + ->filter(fn ($claimerUser) => $claimerUser !== $user) + ->map( + function (User $user) { + + return [ + 'id' => $user->getId(), + 'name' => $user->getName(), + 'icon' => $user->getProfilePictureUrl() + ]; + } + )->toArray() + ] + ); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/AddUserToList.php b/src/php/Routing/Api/Web/AddUserToList.php new file mode 100644 index 0000000..627fc62 --- /dev/null +++ b/src/php/Routing/Api/Web/AddUserToList.php @@ -0,0 +1,61 @@ +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('listid', $body)) { + throw new BadRequestException(); + } + + $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); + if (!$list instanceof GamesList) { + throw new BadRequestException(); + } + + + $requestedUser = $this->entityManager->getRepository(User::class) + ->find($body['requested']); + + if ($list->getClaimer()->contains($requestedUser)) { + return new Response(); + } + $list->addClaimer($requestedUser); + + $this->entityManager->flush(); + + return new Response(); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/RemoveUserFromList.php b/src/php/Routing/Api/Web/RemoveUserFromList.php new file mode 100644 index 0000000..365524e --- /dev/null +++ b/src/php/Routing/Api/Web/RemoveUserFromList.php @@ -0,0 +1,60 @@ +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('listid', $body)) { + throw new BadRequestException(); + } + + $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); + if (!$list instanceof GamesList) { + throw new BadRequestException(); + } + + $requestedUser = $this->entityManager->getRepository(User::class) + ->find($body['requestedUser']); + + if ($requestedUser === $user) { + return new Response(); + } + + $list->getClaimer()->removeElement($requestedUser); + + $this->entityManager->flush(); + + return new Response(); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/SearchForUsers.php b/src/php/Routing/Api/Web/SearchForUsers.php new file mode 100644 index 0000000..dc39bd9 --- /dev/null +++ b/src/php/Routing/Api/Web/SearchForUsers.php @@ -0,0 +1,59 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + $searchQuery = $request->getQueryParams()['query'] ?? ''; + + $repo = $this->entityManager->getRepository(User::class); + + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->contains('name', $searchQuery)); + $criteria->setMaxResults(10); + + $values = $repo->matching($criteria); + return new JsonResponse( + $values + ->filter(fn ($value) => $value !== $user) + ->map(function (User $user) { + return [ + 'value' => $user->getId(), + 'label' => $user->getName() + ]; + }) + ->toArray() + ); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/WebAPIRoutes.php b/src/php/Routing/Api/Web/WebAPIRoutes.php index 350f210..67b6253 100644 --- a/src/php/Routing/Api/Web/WebAPIRoutes.php +++ b/src/php/Routing/Api/Web/WebAPIRoutes.php @@ -14,5 +14,9 @@ final class WebAPIRoutes $group->post('/keys/import/perform', ImportKeysRoute::class); $group->post('/keys/list/create', CreateKeyListRoute::class); + + $group->get('/share/search', SearchForUsers::class); + $group->post('/share/add', AddUserToList::class); + $group->post('/share/remove', RemoveUserFromList::class); } } \ No newline at end of file diff --git a/src/php/Routing/Router.php b/src/php/Routing/Router.php index a0b4ee2..cc33369 100644 --- a/src/php/Routing/Router.php +++ b/src/php/Routing/Router.php @@ -20,21 +20,12 @@ use Psr\Http\Message\ServerRequestInterface; final class Router { - public function __construct( - private ResourceRoute $resourceRoute - ) - { - } - public function route(): ResponseInterface { $request = ServerRequestFactory::fromGlobals( $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES ); - if ($this->resourceRoute->isValid($request->getUri())) { - return $this->resourceRoute->getResponse($request->getUri()); - } - + $router = new \League\Route\Router; $strategy = (new ApplicationStrategy)->setContainer(ContainerHandler::getInstance()); $router->setStrategy($strategy); diff --git a/src/templates/pages/key-manager.php b/src/templates/pages/key-manager.php index 0e444b3..4de5494 100644 --- a/src/templates/pages/key-manager.php +++ b/src/templates/pages/key-manager.php @@ -96,8 +96,7 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]); - +

@@ -107,16 +106,12 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
- +

Users shared to

- - - -
Name
diff --git a/webpack.config.js b/webpack.config.js index 20ecb99..53d38dd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -89,6 +89,9 @@ module.exports = { }, resolve: { extensions: ['.js', '.ts'], + fallback: { + 'bootstrap5-autocomplete/types/autocomplete': 'bootstrap5-autocomplete' + } }, entry: { index: JS_FOLDER + "/pages/index",