diff --git a/public/css/bootstrap-icons-1.10.2.css b/public/css/bootstrap-icons-1.10.2.css
index e7997aa1..dd1401b4 100644
--- a/public/css/bootstrap-icons-1.10.2.css
+++ b/public/css/bootstrap-icons-1.10.2.css
@@ -45,3 +45,4 @@ url("../fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wo
.bi-chevron-contract::before { content: "\f27d"; }
.bi-question-lg::before { content: "\f64e"; }
.bi-filter::before{content:"\f3ca"}
+.bi-pin-map-fill::before{content:"\f64b"}
\ No newline at end of file
diff --git a/public/js/movie.js b/public/js/movie.js
index 86a46aa7..36ab0f26 100644
--- a/public/js/movie.js
+++ b/public/js/movie.js
@@ -104,6 +104,7 @@ function loadWatchDateModal(watchDateListElement) {
document.getElementById('editWatchDateModalPlaysInput').value = watchDateListElement.dataset.plays;
document.getElementById('editWatchDateModalCommentInput').value = watchDateListElement.dataset.comment;
document.getElementById('editWatchDateModalPositionInput').value = watchDateListElement.dataset.position;
+ document.getElementById('editWatchDateModalLocationInput').value = watchDateListElement.dataset.location;
document.getElementById('originalWatchDate').value = watchDateListElement.dataset.watchDate;
document.getElementById('originalWatchDatePlays').value = watchDateListElement.dataset.plays;
@@ -125,6 +126,7 @@ function editWatchDate() {
const newWatchDatePlays = document.getElementById('editWatchDateModalPlaysInput').value;
const newPositionPlays = document.getElementById('editWatchDateModalPositionInput').value;
const comment = document.getElementById('editWatchDateModalCommentInput').value;
+ const location = document.getElementById('editWatchDateModalLocationInput').value;
const apiUrl = '/users/' + getRouteUsername() + '/movies/' + getMovieId() + '/history'
@@ -137,7 +139,8 @@ function editWatchDate() {
'plays': newWatchDatePlays,
'comment': comment,
'position': newPositionPlays,
- 'dateFormat': document.getElementById('dateFormatPhp').value
+ 'dateFormat': document.getElementById('dateFormatPhp').value,
+ 'location': location
}),
success: function (data, textStatus, xhr) {
window.location.reload()
diff --git a/public/js/settings-account-location.js b/public/js/settings-account-location.js
new file mode 100644
index 00000000..7887e8e4
--- /dev/null
+++ b/public/js/settings-account-location.js
@@ -0,0 +1,145 @@
+const locationModal = new bootstrap.Modal('#locationModal', {keyboard: false})
+
+const table = document.getElementById('locationsTable');
+const rows = table.getElementsByTagName('tr');
+
+reloadTable()
+
+async function reloadTable() {
+ table.getElementsByTagName('tbody')[0].innerHTML = ''
+ document.getElementById('locationsTableLoadingSpinner').classList.remove('d-none')
+
+ const response = await fetch('/settings/locations');
+
+ console.log(response.status)
+ if (response.status !== 200) {
+ setLocationsAlert('Could not load locations', 'danger')
+ document.getElementById('locationsTableLoadingSpinner').classList.add('d-none')
+
+ return
+ }
+
+ const locations = await response.json();
+
+ document.getElementById('locationsTableLoadingSpinner').classList.add('d-none')
+
+ locations.forEach((location) => {
+ let row = document.createElement('tr');
+ row.innerHTML = '
' + location.id + ' | ';
+ row.innerHTML += '' + location.name + ' | ';
+ row.style.cursor = 'pointer'
+
+ table.getElementsByTagName('tbody')[0].appendChild(row);
+ })
+
+ registerTableRowClickEvent()
+}
+
+function setLocationsAlert(message, type = 'success') {
+ const locationManagementAlerts = document.getElementById('locationAlerts');
+ locationManagementAlerts.classList.remove('d-none')
+ locationManagementAlerts.innerHTML = ''
+ locationManagementAlerts.innerHTML = '' + message + '
'
+ locationManagementAlerts.style.textAlign = 'center'
+}
+
+function registerTableRowClickEvent() {
+ for (let i = 0; i < rows.length; i++) {
+ if (i === 0) continue
+
+ rows[i].onclick = function () {
+ prepareEditLocationsModal(
+ this.cells[0].innerHTML,
+ this.cells[1].innerHTML,
+ this.cells[2].innerHTML,
+ this.cells[3].innerHTML === '1'
+ )
+
+ locationModal.show()
+ };
+ }
+}
+
+function prepareEditLocationsModal(id, name) {
+ document.getElementById('locationModalHeaderTitle').innerHTML = 'Edit User'
+
+ document.getElementById('locationModalFooterCreateButton').classList.add('d-none')
+ document.getElementById('locationModalFooterButtons').classList.remove('d-none')
+
+ document.getElementById('locationModalNameInput').value = name
+
+ document.getElementById('locationModalAlerts').innerHTML = ''
+
+ // Remove class invalid-input from all (input) elements
+ Array.from(document.querySelectorAll('.invalid-input')).forEach((el) => el.classList.remove('invalid-input'));
+}
+
+function showCreateLocationModal() {
+ prepareCreateLocationModal()
+ locationModal.show()
+}
+
+function prepareCreateLocationModal(name) {
+ document.getElementById('locationModalHeaderTitle').innerHTML = 'Create Location'
+
+ document.getElementById('locationModalFooterCreateButton').classList.remove('d-none')
+ document.getElementById('locationModalFooterButtons').classList.add('d-none')
+
+ document.getElementById('locationModalIdInput').value = ''
+ document.getElementById('locationModalNameInput').value = ''
+
+ document.getElementById('locationModalAlerts').innerHTML = ''
+
+ // Remove class invalid-input from all (input) elements
+ Array.from(document.querySelectorAll('.invalid-input')).forEach((el) => el.classList.remove('invalid-input'));
+}
+
+document.getElementById('createLocationButton').addEventListener('click', async () => {
+ if (validateCreateLocationInput() === true) {
+ return;
+ }
+
+ const response = await fetch('/settings/locations', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ 'name': document.getElementById('locationModalNameInput').value,
+ })
+ })
+
+ if (response.status !== 200) {
+ setLocationModalAlertServerError(await response.text())
+ return
+ }
+
+ setLocationsAlert('Location was created: ' + document.getElementById('locationModalNameInput').value)
+
+ reloadTable()
+ locationModal.hide()
+})
+
+function setLocationModalAlertServerError(message = "Server error, please try again.") {
+ document.getElementById('locationModalAlerts').innerHTML = '' + message + '
'
+}
+
+
+function validateCreateLocationInput() {
+ let error = false
+
+ const nameInput = document.getElementById('locationModalNameInput');
+
+ let mustNotBeEmptyInputs = [nameInput]
+
+ mustNotBeEmptyInputs.forEach((input) => {
+ input.classList.remove('invalid-input');
+ if (input.value.toString() === '') {
+ input.classList.add('invalid-input');
+
+ error = true
+ }
+ })
+
+ return error
+}
diff --git a/settings/routes.php b/settings/routes.php
index b7b9cd58..524d5b96 100644
--- a/settings/routes.php
+++ b/settings/routes.php
@@ -63,6 +63,7 @@ function addWebRoutes(RouterService $routerService, FastRoute\RouteCollector $ro
$routes->add('DELETE', '/settings/account/general/api-token', [Web\SettingsController::class, 'deleteApiToken'], [Web\Middleware\UserIsAuthenticated::class]);
$routes->add('PUT', '/settings/account/general/api-token', [Web\SettingsController::class, 'regenerateApiToken'], [Web\Middleware\UserIsAuthenticated::class]);
$routes->add('GET', '/settings/account/dashboard', [Web\SettingsController::class, 'renderDashboardAccountPage'], [Web\Middleware\UserIsAuthenticated::class]);
+ $routes->add('GET', '/settings/account/locations', [Web\SettingsController::class, 'renderLocationsAccountPage'], [Web\Middleware\UserIsAuthenticated::class]);
$routes->add('GET', '/settings/account/security', [Web\SettingsController::class, 'renderSecurityAccountPage'], [Web\Middleware\UserIsAuthenticated::class]);
$routes->add('GET', '/settings/account/data', [Web\SettingsController::class, 'renderDataAccountPage'], [Web\Middleware\UserIsAuthenticated::class]);
$routes->add('GET', '/settings/server/general', [Web\SettingsController::class, 'renderServerGeneralPage'], [Web\Middleware\UserIsAuthenticated::class]);
diff --git a/src/Domain/Movie/History/Location/MovieHistoryLocationApi.php b/src/Domain/Movie/History/Location/MovieHistoryLocationApi.php
new file mode 100644
index 00000000..4afcbd33
--- /dev/null
+++ b/src/Domain/Movie/History/Location/MovieHistoryLocationApi.php
@@ -0,0 +1,15 @@
+add(MovieHistoryLocationEntity::createFromArray($historyEntry));
+ }
+
+ return $list;
+ }
+
+ private function add(MovieHistoryLocationEntity $dto) : void
+ {
+ $this->data[] = $dto;
+ }
+}
diff --git a/src/HttpController/Web/SettingsController.php b/src/HttpController/Web/SettingsController.php
index 0cb4eaa8..6b3a97c8 100644
--- a/src/HttpController/Web/SettingsController.php
+++ b/src/HttpController/Web/SettingsController.php
@@ -8,6 +8,7 @@
use Movary\Api\Tmdb\Cache\TmdbIsoCountryCache;
use Movary\Api\Trakt\TraktApi;
use Movary\Domain\Movie;
+use Movary\Domain\Movie\History\Location\MovieHistoryLocationApi;
use Movary\Domain\User;
use Movary\Domain\User\Service\Authentication;
use Movary\Domain\User\Service\TwoFactorAuthenticationApi;
@@ -52,6 +53,7 @@ public function __construct(
private readonly DashboardFactory $dashboardFactory,
private readonly EmailService $emailService,
private readonly TmdbIsoCountryCache $countryCache,
+ private readonly MovieHistoryLocationApi $historyLocationApi,
) {
}
@@ -176,6 +178,20 @@ public function renderDashboardAccountPage() : Response
);
}
+ public function renderLocationsAccountPage() : Response
+ {
+ $user = $this->authenticationService->getCurrentUser();
+
+ $locations = $this->historyLocationApi->findLocationsByUserId($user->getId());
+
+ return Response::create(
+ StatusCode::createOk(),
+ $this->twig->render('page/settings-account-locations.html.twig', [
+ 'locations' => $locations,
+ ]),
+ );
+ }
+
public function renderDataAccountPage() : Response
{
$userId = $this->authenticationService->getCurrentUserId();
diff --git a/templates/component/modal-edit-watch-date.html.twig b/templates/component/modal-edit-watch-date.html.twig
index a0647930..36373533 100644
--- a/templates/component/modal-edit-watch-date.html.twig
+++ b/templates/component/modal-edit-watch-date.html.twig
@@ -23,7 +23,11 @@
-
+
+
+
+
+
diff --git a/templates/component/settings-nav.html.twig b/templates/component/settings-nav.html.twig
index 429b71e9..c8ce4c85 100644
--- a/templates/component/settings-nav.html.twig
+++ b/templates/component/settings-nav.html.twig
@@ -10,6 +10,9 @@
Security
+
+ Locations
+
Dashboard
diff --git a/templates/page/movie.html.twig b/templates/page/movie.html.twig
index 05846ad6..489ba6c7 100644
--- a/templates/page/movie.html.twig
+++ b/templates/page/movie.html.twig
@@ -135,7 +135,8 @@
data-watch-date="{{ watchDate.watched_at is null ? '' : watchDate.watched_at|date(dateFormatPhp) }}"
data-plays="{{ watchDate.plays }}"
data-comment="{{ watchDate.comment }}"
- data-position="{{ watchDate.position }}">
+ data-position="{{ watchDate.position }}"
+ data-location="{{ watchDate.location_id }}">
{{ watchDate.watched_at is null ? 'Unkown date' : watchDate.watched_at|date(dateFormatPhp) }} {% if watchDate.plays > 1 %}({{ watchDate.plays }}x){% endif %}
{% if watchDate.comment != '' %}{% endif %}
diff --git a/templates/page/settings-account-locations.html.twig b/templates/page/settings-account-locations.html.twig
new file mode 100644
index 00000000..2a9d0d32
--- /dev/null
+++ b/templates/page/settings-account-locations.html.twig
@@ -0,0 +1,52 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}
+ Settings - General Account
+{% endblock %}
+
+{% block stylesheets %}
+
+{% endblock %}
+
+{% block scripts %}
+
+{% endblock %}
+
+{% block body %}
+
+ {{ include('component/navbar.html.twig') }}
+
+
+ {{ include('component/settings-nav.html.twig') }}
+
+
+
Locations
+
+
+
Customize your available location options.
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
+ {{ include('component/modal-location.html.twig') }}
+
+{% endblock %}