summaryrefslogtreecommitdiff
path: root/pse-dashboard/src/views
diff options
context:
space:
mode:
authorOrangerot <purple@orangerot.dev>2024-06-19 00:14:49 +0200
committerOrangerot <purple@orangerot.dev>2024-06-27 12:11:14 +0200
commit5b8851b6c268d0e93c158908fbfae9f8473db5ff (patch)
tree7010eb85d86fa2da06ea4ffbcdb01a685d502ae8 /pse-dashboard/src/views
Initial commitHEADmain
Diffstat (limited to 'pse-dashboard/src/views')
-rw-r--r--pse-dashboard/src/views/EpisodesView.vue42
-rw-r--r--pse-dashboard/src/views/ForgotPasswordView.vue52
-rw-r--r--pse-dashboard/src/views/LoginView.vue91
-rw-r--r--pse-dashboard/src/views/RegistrationView.vue78
-rw-r--r--pse-dashboard/src/views/ResetPasswordView.vue72
-rw-r--r--pse-dashboard/src/views/SettingsView.vue347
-rw-r--r--pse-dashboard/src/views/SubscriptionsView.vue270
-rw-r--r--pse-dashboard/src/views/index.js18
8 files changed, 970 insertions, 0 deletions
diff --git a/pse-dashboard/src/views/EpisodesView.vue b/pse-dashboard/src/views/EpisodesView.vue
new file mode 100644
index 0000000..a7027b1
--- /dev/null
+++ b/pse-dashboard/src/views/EpisodesView.vue
@@ -0,0 +1,42 @@
+<script setup>
+import { DashboardLayout, EpisodeEntry, LoadingConditional } from '@/components'
+import { ref, onMounted } from 'vue';
+import { getEpisodeActions } from '@/api/pse-squared.js';
+
+const episodes = ref(null);
+const received = ref(false);
+
+onMounted(async () => {
+ try {
+ const response = await getEpisodeActions();
+
+ received.value = true;
+ episodes.value = response.data;
+ } catch(err) {
+ }
+});
+
+</script>
+<template>
+ <DashboardLayout>
+ <h1 class="h1 mb-4">
+ {{ $t("message.mostRecentlyHeardEpisodes") }}
+ </h1>
+
+ <LoadingConditional :waiting-for="received">
+ <div class="list-group w-auto">
+ <EpisodeEntry
+ v-for="(action, index) in episodes.actions"
+ :key="index"
+ :action="action"
+ />
+ </div>
+ <p v-if="episodes.actions.length == 0">
+ {{ $t('message.noEpisodes') }}
+ </p>
+ </LoadingConditional>
+ </DashboardLayout>
+</template>
+<style>
+</style>
+
diff --git a/pse-dashboard/src/views/ForgotPasswordView.vue b/pse-dashboard/src/views/ForgotPasswordView.vue
new file mode 100644
index 0000000..03d0ff1
--- /dev/null
+++ b/pse-dashboard/src/views/ForgotPasswordView.vue
@@ -0,0 +1,52 @@
+<script setup>
+import { FloatingLabelInput, FormLayout } from '@/components'
+import { useLogger } from '@/logger.js'
+import { ref } from 'vue'
+import { forgotPassword } from '@/api/pse-squared.js'
+
+const email = ref("");
+
+const { passwordForgot } = useLogger();
+
+async function formForgot() {
+ try {
+ await forgotPassword({ email: email.value });
+ passwordForgot();
+ } catch (err) {}
+}
+</script>
+<template>
+ <FormLayout>
+ <!-- Text über Texteingabefeld -->
+ <h1 class="h3 mb-3 fw-normal">
+ {{ $t("message.emailAddressRequest") }}
+ </h1>
+
+ <form @submit.prevent="formForgot">
+ <!-- Eingabefeld für E-Mail-Adresse -->
+ <FloatingLabelInput
+ v-model="email"
+ type="email"
+ :label="$t('form.emailAddress')"
+ />
+
+ <!-- Absende Knopf der Daten -->
+ <button
+ type="submit"
+ class="w-100 btn btn-lg btn-primary mt-2"
+ >
+ {{ $t("message.send") }}
+ </button>
+ </form>
+
+ <!-- Zurück zur Anmeldung -->
+ <router-link to="/">
+ <button class="w-100 btn btn-lg btn-secondary mt-2">
+ {{ $t("message.close") }}
+ </button>
+ </router-link>
+ </FormLayout>
+</template>
+<style scoped>
+</style>
+
diff --git a/pse-dashboard/src/views/LoginView.vue b/pse-dashboard/src/views/LoginView.vue
new file mode 100644
index 0000000..303db3a
--- /dev/null
+++ b/pse-dashboard/src/views/LoginView.vue
@@ -0,0 +1,91 @@
+<script setup>
+import { ref } from 'vue';
+import { FloatingLabelInput, FormLayout, PasswordInput } from '@/components'
+import { store } from '@/store.js';
+import router from '@/router.js';
+
+const username = ref("");
+const password = ref("");
+const stayLoggedIn = ref(false);
+
+async function login(e) {
+ const success = await store.login({
+ username: username.value,
+ password: password.value
+ }, stayLoggedIn.value);
+
+ if (success) {
+ router.push("/subscriptions");
+ }
+}
+
+</script>
+<template>
+ <FormLayout>
+ <!-- Text über Texteingabe -->
+ <h1 class="h3 mb-3 fw-normal">
+ {{ $t("message.loginRequest") }}
+ </h1>
+
+ <form @submit.prevent="login">
+ <!-- Eingabefeld für Nutzername -->
+ <FloatingLabelInput
+ v-model="username"
+ :label="$t('form.username')"
+ />
+
+ <!-- Eingabefeld für Passwort -->
+ <PasswordInput
+ v-model="password"
+ :label="$t('form.password')"
+ />
+
+ <div class="row">
+ <!-- Angemeldet bleiben Checkbox -->
+ <div class="col-6">
+ <div class="checkbox mb-3">
+ <label>
+ <input
+ v-model="stayLoggedIn"
+ type="checkbox"
+ value="remember-me"
+ >
+ {{ $t("message.rememberMe") }}
+ </label>
+ </div>
+ </div>
+
+ <!-- Passwort vergessen Link -->
+ <div class="col-6">
+ <router-link to="/forgotPassword">
+ {{ $t("message.forgotPassword") }}?
+ </router-link>
+ </div>
+ </div>
+
+ <!-- Knopf um sich anzumelden -->
+ <button
+ type="submit"
+ class="w-100 btn btn-lg btn-primary"
+ >
+ {{ $t("message.login") }}
+ </button>
+
+ <!-- Registrieren Link -->
+ <p class="mt-1">
+ {{ $t("message.noAccountYet") }}?
+ <router-link to="/registration">
+ {{ $t("message.signUp") }}
+ </router-link>
+ </p>
+ </form>
+
+ <!-- Footer -->
+ <p class="mt-5 mb-3 text-muted">
+ &copy; 2023
+ </p>
+ </FormLayout>
+</template>
+<style scoped>
+</style>
+
diff --git a/pse-dashboard/src/views/RegistrationView.vue b/pse-dashboard/src/views/RegistrationView.vue
new file mode 100644
index 0000000..2d561f3
--- /dev/null
+++ b/pse-dashboard/src/views/RegistrationView.vue
@@ -0,0 +1,78 @@
+<script setup>
+import { FloatingLabelInput, FormLayout, PasswordValidator } from '@/components'
+import { ref } from 'vue';
+import router from '@/router.js';
+import { register } from '@/api/pse-squared.js'
+import { useLogger } from '@/logger.js'
+
+const username = ref("");
+const email = ref("");
+const passwordModel = ref(null);
+
+const { passwordRequirements, accountCreated } = useLogger();
+
+async function formRegister() {
+ if(!passwordModel.value.valid) {
+ // log.append({type: "info", message: "Password requirements are not met"})
+ passwordRequirements();
+ return;
+ }
+
+ try {
+ await register({
+ username: username.value,
+ email: email.value,
+ password: passwordModel.value.password
+ });
+
+ accountCreated();
+ router.push("/login");
+ } catch (err) {
+ }
+}
+
+</script>
+<template>
+ <FormLayout>
+ <!-- Text über Texteingabefeld -->
+ <h1 class="h3 mb-3 fw-normal">
+ {{ $t("message.registration") }}
+ </h1>
+
+ <form @submit.prevent="formRegister">
+ <!-- Eingabefeld für Nutzernamen -->
+ <FloatingLabelInput
+ v-model="username"
+ type="text"
+ :label="$t('form.username')"
+ />
+
+ <!-- Eingabefeld für E-Mail-Adresse -->
+ <FloatingLabelInput
+ v-model="email"
+ type="email"
+ :label="$t('form.emailAddress')"
+ />
+
+ <!-- Passwort-Validierungs-Komponente -->
+ <PasswordValidator v-model="passwordModel" />
+
+ <!-- Absende Knopf für E-Mail-Adresse -->
+ <button
+ type="submit"
+ class="w-100 btn btn-lg btn-primary"
+ >
+ {{ $t("message.signUp") }}
+ </button>
+ </form>
+
+ <!-- Zurück zur Anmeldung -->
+ <router-link to="/">
+ <button class="w-100 btn btn-lg btn-secondary mt-2">
+ {{ $t("message.close") }}
+ </button>
+ </router-link>
+ </FormLayout>
+</template>
+<style scoped>
+</style>
diff --git a/pse-dashboard/src/views/ResetPasswordView.vue b/pse-dashboard/src/views/ResetPasswordView.vue
new file mode 100644
index 0000000..a617af3
--- /dev/null
+++ b/pse-dashboard/src/views/ResetPasswordView.vue
@@ -0,0 +1,72 @@
+<script setup>
+import { FormLayout, PasswordValidator } from '@/components'
+import { ref } from 'vue'
+import { useLogger } from '@/logger.js'
+import { resetPassword } from '@/api/pse-squared.js'
+import router from '@/router.js';
+
+const props = defineProps({
+ token: {
+ type: String,
+ default: ""
+ },
+ username: {
+ type: String,
+ default: ""
+ }
+});
+
+const { passwordRequirements, passwordReset } = useLogger();
+
+const passwordModel = ref(null);
+
+async function formResetPassword() {
+ if(!passwordModel.value.valid) {
+ // log.append({type: "info", message: "Password requirements are not met"})
+ passwordRequirements();
+ return;
+ }
+
+ try {
+ await resetPassword({
+ username: props.username,
+ password: passwordModel.value.password,
+ token: props.token
+ });
+
+ passwordReset();
+ router.push("/login");
+ } catch (err) {}
+}
+
+</script>
+<template>
+ <FormLayout>
+ <form @submit.prevent="formResetPassword">
+ <!-- Text über Texteingabefeld -->
+ <h1 class="h3 mb-3 fw-normal">
+ {{ $t("message.setNewPassword") }}
+ </h1>
+
+ <!-- Passwort-Validierungs-Komponente -->
+ <PasswordValidator v-model="passwordModel" />
+
+ <!-- Absende Knopf für E-Mail-Adresse -->
+ <button
+ type="submit"
+ class="w-100 btn btn-lg btn-primary"
+ >
+ {{ $t("message.send") }}
+ </button>
+
+ <!-- Zurück zur Anmeldung -->
+ <router-link to="/">
+ <button class="w-100 btn btn-lg btn-secondary mt-2">
+ {{ $t("message.close") }}
+ </button>
+ </router-link>
+ </form>
+ </FormLayout>
+</template>
+<style scoped>
+</style>
diff --git a/pse-dashboard/src/views/SettingsView.vue b/pse-dashboard/src/views/SettingsView.vue
new file mode 100644
index 0000000..4e5e486
--- /dev/null
+++ b/pse-dashboard/src/views/SettingsView.vue
@@ -0,0 +1,347 @@
+<script setup>
+import {
+ DashboardLayout,
+ FloatingLabelInput,
+ PasswordInput,
+ PasswordValidator,
+} from '@/components'
+import { ref } from 'vue';
+import { store } from '@/store.js'
+import useGPodder from '@/api/gpodder.js'
+import {
+ changePassword,
+ deleteAccount,
+ getEpisodeActions,
+ getTitles,
+ postEpisodeActions,
+ putSubscriptions,
+} from '@/api/pse-squared.js'
+import JSZip from 'jszip';
+import router from '@/router.js'
+import { useLogger } from '@/logger.js'
+import { saveAs } from 'file-saver';
+
+/******************************************************************************/
+/* change password */
+/******************************************************************************/
+
+const changePasswordOld = ref(null);
+const changePasswordNew = ref(null);
+
+const logger = useLogger();
+
+async function formChangePassword() {
+ if(!changePasswordNew.value.valid) {
+ logger.passwordRequirements();
+ return;
+ }
+
+ try {
+ await changePassword({
+ password: changePasswordOld.value,
+ new_password: changePasswordNew.value.password
+ });
+
+ await store.login({
+ username: store.username,
+ password: changePasswordNew.value.password
+ }, false);
+
+ changePasswordOld.value = "";
+ changePasswordNew.value = {password: "", valid: false};
+
+ logger.passwordChanged();
+ } catch (err) {}
+}
+
+/******************************************************************************/
+/* delete account */
+/******************************************************************************/
+
+const deletePassword = ref("");
+
+async function formDelete() {
+
+ // FIXME: backend
+ if (store.password != deletePassword.value) {
+ logger.badRequestError();
+ return;
+ }
+
+ try {
+ await deleteAccount({
+ password: deletePassword.value
+ });
+
+ logger.accountDeleted();
+ store.logout();
+ router.push("/login");
+ } catch (err) {}
+}
+
+/******************************************************************************/
+/* import from another GPodder instance */
+/******************************************************************************/
+
+const gPodderInstance = ref("");
+const gPodderUsername = ref("");
+const gPodderPassword = ref("");
+
+async function formImportGPodder() {
+ const instance = useGPodder({
+ baseURL: gPodderInstance.value
+ });
+
+ await instance.login({
+ username: gPodderUsername.value,
+ password: gPodderPassword.value
+ });
+
+ // download titles from instance and upload to pse-squared
+ try {
+ const response = await instance.getTitles();
+ const subscriptions = response.data.map(sub => sub.url);
+
+ await putSubscriptions(subscriptions);
+ } catch (err) {
+ console.error(err);
+ return;
+ }
+
+ // download episodes from instance and upload to pse-squared
+ try {
+ const response = await instance.getEpisodeActions();
+ const episodes = response.data.actions;
+
+ await postEpisodeActions(episodes);
+ } catch (err) {
+ console.error(err);
+ return;
+ }
+
+ logger.gpodderImport();
+}
+
+
+/******************************************************************************/
+/* import and export data */
+/******************************************************************************/
+
+async function importData(e) {
+ const file = e.target.files[0];
+
+ const zip = await JSZip.loadAsync(file);
+
+ // read and upload subscriptions
+ try {
+ const subscriptionsText = await zip.files["subscriptions.json"].async("string");
+ const subscriptions = JSON.parse(subscriptionsText).map(sub => sub.url);
+ console.log({subscriptions});
+
+ await putSubscriptions(subscriptions);
+ } catch(err) {
+ console.error(err);
+ }
+
+ // read and upload episodeActions
+ try {
+ const episodeActionsText = await zip.files["episodeActions.json"].async("string");
+ const episodeActions = JSON.parse(episodeActionsText).actions;
+
+ await postEpisodeActions(episodeActions);
+ } catch(err) {
+ console.error(err);
+ }
+}
+
+async function exportData() {
+ const zip = new JSZip();
+
+ // load subscriptions
+ try {
+ const subscriptionResponse = await getTitles();
+ zip.file("subscriptions.json", JSON.stringify(subscriptionResponse.data));
+ } catch(err) {
+ console.error(err);
+ }
+
+ // load episodeActions
+ try {
+ const episodeActionsResponse = await getEpisodeActions();
+ zip.file("episode-actions.json", JSON.stringify(episodeActionsResponse.data));
+ } catch(err) {
+ console.error(err);
+ }
+
+ // generate and save zip
+ zip.generateAsync({type:"blob"}).then(function(content) {
+ saveAs(content, "pse-export.zip");
+ });
+}
+
+</script>
+<template>
+ <DashboardLayout>
+ <!-- Überschrift -->
+ <h1 class="h1 mb-4">
+ {{ $t("message.settings") }}
+ </h1>
+
+ <!-- Passwort ändern Sektion -->
+ <form
+ class="mb-4"
+ @submit.prevent="formChangePassword"
+ >
+ <h2>{{ $t("message.changePassword") }}</h2>
+ <!-- Altes Passwort -->
+ <PasswordInput
+ v-model="changePasswordOld"
+ :label="$t('message.oldPassword')"
+ />
+
+ <!-- Neues Passwort -->
+ <PasswordValidator v-model="changePasswordNew" />
+
+ <button
+ type="submit"
+ class="btn btn-primary"
+ >
+ {{ $t("message.changePassword") }}
+ </button>
+ </form>
+
+ <!-- Gpodder-Import Sektion -->
+ <form
+ class="mb-4"
+ @submit.prevent="formImportGPodder"
+ >
+ <h2>Gpodder-{{ $t("message.import") }}</h2>
+
+ <!-- Gpodder Instanz -->
+ <FloatingLabelInput
+ v-model="gPodderInstance"
+ :label="$t('message.instance')"
+ />
+
+ <!-- Nutzername -->
+ <FloatingLabelInput
+ v-model="gPodderUsername"
+ :label="$t('form.username')"
+ />
+
+ <!-- Passwort -->
+ <PasswordInput
+ v-model="gPodderPassword"
+ :label="$t('form.password')"
+ />
+
+ <button
+ type="submit"
+ class="btn btn-primary"
+ >
+ {{ $t("message.importData") }}
+ </button>
+ </form>
+
+ <!-- Personenbezogene Daten im-/exportieren -->
+ <div class="mb-4">
+ <h2>{{ $t("message.personalData") }}</h2>
+ <label
+ for="file"
+ class="btn btn-success"
+ >
+ {{ $t("message.importData") }}
+ </label>
+ <input
+ id="file"
+ type="file"
+ accept=".zip,application/zip"
+ hidden
+ @change="importData"
+ >
+ <button
+ class="btn btn-warning mx-2"
+ @click="exportData"
+ >
+ {{ $t("message.exportData") }}
+ </button>
+ </div>
+
+ <!-- Account löschen -->
+ <div class="mb-4">
+ <h2>{{ $t("message.deleteAccount") }}</h2>
+
+ <button
+ class="btn btn-danger"
+ data-bs-toggle="modal"
+ data-bs-target="#delete-user"
+ >
+ {{ $t("message.deleteAccount") }}
+ </button>
+ </div>
+ </DashboardLayout>
+
+ <!-- Modal for confirming deletion of user -->
+ <div
+ id="delete-user"
+ class="modal"
+ tabindex="-1"
+ >
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <!-- Title of Modal -->
+ <div class="modal-header">
+ <h5 class="modal-title">
+ {{ $t('message.deleteAccount') }}
+ </h5>
+ <button
+ type="button"
+ class="btn-close"
+ data-bs-dismiss="modal"
+ aria-label="Close"
+ />
+ </div>
+
+ <!-- list podcasts to unsubscribe from -->
+ <div class="modal-body">
+ <div
+ class="alert alert-warning d-flex align-items-center"
+ role="alert"
+ >
+ <i class="fs-2 me-3 fa fa-warning" />
+ <div>
+ {{ $t('message.deleteAccountWarning', {username: store.username}) }}
+ </div>
+ </div>
+
+ <PasswordInput
+ v-model="deletePassword"
+ :label="$t('form.password')"
+ />
+ </div>
+
+ <!-- buttons to dismiss or finaly unsubscribe -->
+ <div class="modal-footer">
+ <button
+ type="button"
+ class="btn btn-secondary"
+ data-bs-dismiss="modal"
+ >
+ {{ $t('message.close') }}
+ </button>
+ <button
+ type="button"
+ data-bs-dismiss="modal"
+ class="btn btn-danger"
+ @click="formDelete"
+ >
+ {{ $t('message.deleteAccount') }}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<style scoped>
+</style>
+
diff --git a/pse-dashboard/src/views/SubscriptionsView.vue b/pse-dashboard/src/views/SubscriptionsView.vue
new file mode 100644
index 0000000..045b5f1
--- /dev/null
+++ b/pse-dashboard/src/views/SubscriptionsView.vue
@@ -0,0 +1,270 @@
+<script setup>
+import { DashboardLayout, FloatingLabelInput, LoadingConditional, SubscriptionEntry } from '@/components';
+import { useLogger } from '@/logger.js'
+import { ref, onMounted } from 'vue';
+import { getTitles, putSubscriptions, postSubscriptions } from '@/api/pse-squared.js'
+import { Modal } from 'bootstrap';
+
+const titles = ref(null);
+const received = ref(false);
+
+const newSubscription = ref();
+const deletedSubscriptions = ref([]);
+
+const { subscriptionAdded } = useLogger();
+
+// if called from pseudo-protocol: show dialog to add subscription up front
+onMounted(() => {
+ loadData();
+
+ const urlParams = new URLSearchParams(window.location.search);
+
+ const addSubModal = new Modal("#add-sub");
+ if (urlParams.has('add')) {
+ newSubscription.value = decodeURIComponent(urlParams.get('add'))
+ .replace("web+pod://", "");
+ addSubModal.show();
+ }
+});
+
+// fetches all titles of the subscribed podcasts and sets visibility
+async function loadData() {
+ try {
+ const response = await getTitles();
+
+ received.value = true;
+ titles.value = response.data;
+ } catch(err) { }
+}
+
+// makes a addition request based in the url in the input field/pseudo protocoll
+async function addSubscription() {
+ received.value = false;
+ await putSubscriptions([newSubscription.value]);
+
+ // log.append({type: "info", message: "Subscription got added to your list!"});
+ subscriptionAdded();
+ newSubscription.value = "";
+
+ await loadData();
+}
+
+// toggles the selection of all subscriptions
+function selectAllSubscriptions() {
+ if ( deletedSubscriptions.value.length > 0 ) {
+ deletedSubscriptions.value = [];
+ } else {
+ deletedSubscriptions.value = titles.value;
+ }
+}
+
+// makes a deletion request from all selected podcasts
+async function unsubscribeFromSelected() {
+ received.value = false;
+
+ await postSubscriptions({
+ add: [],
+ remove: deletedSubscriptions.value.map(e => e.url)
+ });
+ deletedSubscriptions.value = [];
+
+ await loadData();
+}
+
+// the modal gets open by the id from the html attributes defined in the Subscription
+// component.
+function unsubscribeSingle(sub) {
+ deletedSubscriptions.value = [sub];
+}
+
+</script>
+<template>
+ <DashboardLayout>
+ <h1 class="h1 mb-4">
+ {{ $t("message.yourSubscriptions") }}
+ </h1>
+
+ <!-- add a new subscription by url -->
+ <form
+ class="input-group mb-3"
+ @submit.prevent="addSubscription"
+ >
+ <FloatingLabelInput
+ v-model="newSubscription"
+ :label="$t('message.newSubscription')"
+ />
+ <button
+ class="btn btn-success"
+ type="submit"
+ >
+ {{ $t("message.addSubscription") }}
+ </button>
+ </form>
+
+ <LoadingConditional :waiting-for="received">
+ <!-- user does not have subscriptions yet -->
+ <p v-if="titles.length == 0">
+ {{ $t("message.noSubscriptions") }}
+ </p>
+
+ <!-- display subscriptions -->
+ <div v-else>
+ <button
+ class="btn m-2 btn-success"
+ @click="selectAllSubscriptions"
+ >
+ {{ $t('message.selectAll') }}
+ </button>
+ <button
+ class="btn m-2 btn-primary"
+ :class="{disabled: deletedSubscriptions.length == 0}"
+ data-bs-toggle="modal"
+ data-bs-target="#delete-subs"
+ >
+ {{ $t('message.unsubscribeSelected') }}
+ </button>
+
+ <div id="episodes-accordion">
+ <div
+ v-for="sub in titles"
+ :key="sub.url"
+ class="form-check"
+ >
+ <input
+ v-model="deletedSubscriptions"
+ class="form-check-input mt-4"
+ type="checkbox"
+ :value="sub"
+ >
+ <SubscriptionEntry
+ :sub="sub"
+ @unsubscribe="unsubscribeSingle"
+ />
+ </div>
+ </div>
+ </div>
+ </LoadingConditional>
+ </DashboardLayout>
+
+ <!-- Modal for adding subscription with pseudo-protocol -->
+ <div
+ id="add-sub"
+ class="modal"
+ tabindex="-1"
+ >
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <!-- Title of Modal -->
+ <div class="modal-header">
+ <h5 class="modal-title">
+ {{ $t('message.newSubscription') }}
+ </h5>
+ <button
+ type="button"
+ class="btn-close"
+ data-bs-dismiss="modal"
+ aria-label="Close"
+ />
+ </div>
+
+ <!-- display URL of Podcast to subcribe to -->
+ <div class="modal-body">
+ <FloatingLabelInput
+ v-model="newSubscription"
+ :label="$t('message.newSubscription')"
+ />
+ </div>
+
+ <!-- buttons to dismiss or accept the new subscription -->
+ <div class="modal-footer">
+ <button
+ type="button"
+ class="btn btn-secondary"
+ data-bs-dismiss="modal"
+ >
+ {{ $t('message.close') }}
+ </button>
+ <button
+ type="button"
+ data-bs-dismiss="modal"
+ class="btn btn-primary"
+ @click="addSubscription"
+ >
+ {{ $t('message.addSubscription') }}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Modal for confirming deletion of subscriptions -->
+ <div
+ id="delete-subs"
+ class="modal"
+ tabindex="-1"
+ >
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <!-- Title of Modal -->
+ <div class="modal-header">
+ <h5 class="modal-title">
+ {{ $t('message.unsubscribePodcasts') }}
+ </h5>
+ <button
+ type="button"
+ class="btn-close"
+ data-bs-dismiss="modal"
+ aria-label="Close"
+ />
+ </div>
+
+ <!-- list podcasts to unsubscribe from -->
+ <div class="modal-body">
+ <div
+ class="alert alert-warning d-flex align-items-center"
+ role="alert"
+ >
+ <i class="fs-2 me-3 fa fa-warning" />
+ <div>
+ {{ $t('message.unsubscribePodcastsWarning') }}
+ </div>
+ </div>
+
+ <ul>
+ <li
+ v-for="sub in deletedSubscriptions"
+ :key="sub.url"
+ >
+ {{ sub.title || sub.url }}
+ <span class="opacity-50">
+ ({{ $t('message.episode', sub.episodes.length) }})
+ </span>
+ </li>
+ </ul>
+ </div>
+
+ <!-- buttons to dismiss or finaly unsubscribe -->
+ <div class="modal-footer">
+ <button
+ type="button"
+ class="btn btn-secondary"
+ data-bs-dismiss="modal"
+ >
+ {{ $t('message.close') }}
+ </button>
+ <button
+ type="button"
+ data-bs-dismiss="modal"
+ class="btn btn-primary"
+ @click="unsubscribeFromSelected"
+ >
+ {{ $t('message.unsubscribePodcasts') }}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<style scoped>
+</style>
+
diff --git a/pse-dashboard/src/views/index.js b/pse-dashboard/src/views/index.js
new file mode 100644
index 0000000..e4706f8
--- /dev/null
+++ b/pse-dashboard/src/views/index.js
@@ -0,0 +1,18 @@
+import EpisodesView from './EpisodesView.vue'
+import ForgotPasswordView from './ForgotPasswordView.vue'
+import LoginView from './LoginView.vue'
+import RegistrationView from './RegistrationView.vue'
+import ResetPasswordView from './ResetPasswordView.vue'
+import SettingsView from './SettingsView.vue'
+import SubscriptionsView from './SubscriptionsView.vue'
+
+export {
+ EpisodesView,
+ ForgotPasswordView,
+ LoginView,
+ RegistrationView,
+ ResetPasswordView,
+ SettingsView,
+ SubscriptionsView,
+}
+