Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: persist state per site #49

Merged
merged 3 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"reselect": "5.1.0",
"query-string": "9.0.0",
"@heroicons/vue": "2.1.3",
"@headlessui/vue": "1.7.22",
"vue-i18n": "9.10.2",
"floating-vue": "5.2.2"
},
Expand Down
26 changes: 26 additions & 0 deletions client/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions client/src/components/modal/Modal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<div>
<TransitionRoot as="template" :show="visible">
<Dialog class="relative z-50" @close="emit('close')">
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in duration-200"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</TransitionChild>

<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
>
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<DialogPanel
class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"
>
<div class="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
<button
type="button"
class="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
@click="emit('close')"
>
<span class="sr-only">Close</span>
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<div class="mt-2">
<slot></slot>
</div>
</div>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</div>
</template>

<script setup lang="ts">
import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue';
import { XMarkIcon } from '@heroicons/vue/24/outline';

defineProps<{ visible: boolean; }>();
const emit = defineEmits(['close']);
</script>
48 changes: 48 additions & 0 deletions client/src/features/notifications/Notifications.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>
<!-- Global notification live region, render this permanently at the end of the document -->
<div
aria-live="assertive"
class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50"
>
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
<transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
v-for="(notification, index) in state.notifications"
>
<NotificationComponent
v-if="notification.visible"
:type="notification.type"
:title="notification.title"
:details="notification.details"
@close="closeNotification(index)"
/>
</transition>
</div>
</div>
</template>

<script setup lang="ts">
import { type Action } from 'redux';
import { injectStore, mapState } from 'redux-vuex';
import { actions, selectors } from '../../store';
import { type Notification } from '../../types/notification.types';
import NotificationComponent from './components/Notification.vue';

const store = injectStore();

const state = mapState<{
notifications: Notification[]
}>({
notifications: selectors.notifications
});

const closeNotification = (index: number) => {
store.dispatch(actions.notifications.hide(index) as Action);
};
</script>
74 changes: 74 additions & 0 deletions client/src/features/notifications/components/Notification.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<template>
<div
class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5"
>
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<InformationCircleIcon
v-if="type === 'info'"
class="h-6 w-6"
:class="textColor"
aria-hidden="true"
/>
<ExclamationCircleIcon
v-if="type === 'error'"
class="h-6 w-6"
:class="textColor"
aria-hidden="true"
/>
<ExclamationCircleIcon
v-if="type === 'warning'"
class="h-6 w-6"
:class="textColor"
aria-hidden="true"
/>
<CheckCircleIcon
v-if="type === 'success'"
class="h-6 w-6"
:class="textColor"
aria-hidden="true"
/>
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium" :class="textColor">{{ title }}</p>
<p class="mt-1 text-sm text-gray-500" v-if="details">
{{ details }}
</p>
</div>
<div class="ml-4 flex flex-shrink-0">
<button
type="button"
@click="emit('close')"
class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<span class="sr-only">Close</span>
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { InformationCircleIcon, ExclamationCircleIcon, CheckCircleIcon } from '@heroicons/vue/24/outline';
import { XMarkIcon } from '@heroicons/vue/20/solid';
import { type Notification } from '../../../types/notification.types';
import { computed } from 'vue';

const props = defineProps<{ type: Notification['type']; title: string; details?: string }>();
const emit = defineEmits(['close']);
const textColor = computed(() => {
switch (props.type) {
case 'info':
return 'text-gray-900';
case 'error':
return 'text-red-600';
case 'warning':
return 'text-yellow-600';
case 'success':
return 'text-green-600';
}
});
</script>
18 changes: 17 additions & 1 deletion client/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default {
description:
'In den folgenden Schritten kannst Deinen bestehenden Podcast zu Podlove umziehen. Gib einfach die RSS-Feed-URL deines aktuellen Podcasts unten ein. Die RSS-Feed-URL ist ein spezieller Link, der den Inhalt deines Podcasts zusammenfasst und es Plattformen ermöglicht, diesen Inhalt zu lesen und anzuzeigen.',
help: 'Die RSS-Feed-URL findest Du normalerweise in den Einstellungen oder im Dashboard Deiner aktuellen Hosting-Plattform. Hinweise, wie Du die RSS-Feed-URL bei einzelnen Hosting-Plattformen finden kannst, haben wir und die Community in diesem <a class="support-link" href="https://sendegate.de/t/podcast-feed-bei-hosting-plattformen-finden-wiki/17116">Post</a> im Sendegate zusammengefasst.',
'feed-url': 'Podcast feed url',
'feed-url': 'Podcast feed URL',
'feed-url-placeholder': 'Podcast-Feed-URL eingeben',
'success-head': 'Super!',
'success-info':
Expand Down Expand Up @@ -166,6 +166,22 @@ export default {
playButton: 'Staffel 2, Ep 1',
description:
'[Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.]'
},
error: {
podcast: {
metadata: {
title: 'Anfrage fehlgeschlagen',
details: 'Fehler beim abfragen der Podcast Metadaten'
},
episodes: {
title: 'Anfrage fehlgeschlagen',
details: 'Fehler beim abfragen der Podcast Episoden'
},
feedUrl: {
title: 'Anfrage fehlgeschlagen',
details: 'Fehler beim Abfragen der Feed Url'
}
}
}
},
select: {
Expand Down
18 changes: 17 additions & 1 deletion client/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default {
description:
"Let's get started on bringing your existing podcast over to Podlove. Simply enter the RSS feed URL of your current podcast below.",
help: 'You can usually find the RSS feed URL in the settings or dashboard of your current hosting platform. For more detailed instructions on finding your RSS feed URL with popular hosting platforms, check out this <a class="support-link" href="https://sendegate.de/t/podcast-feed-bei-hosting-plattformen-finden-wiki/17116">post</a> in the Sendegate.',
'feed-url': 'Podcast feed url',
'feed-url': 'Podcast feed URL',
'feed-url-placeholder': 'Enter the feed url',
'success-head': 'Success!',
'success-info':
Expand Down Expand Up @@ -167,6 +167,22 @@ export default {
playButton: 'Season 2, Ep 1',
description:
'[Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.]'
},
error: {
podcast: {
metadata: {
title: 'Request failed',
details: 'Error while fetching podcast metadata'
},
episodes: {
title: 'Request failed',
details: 'Error while fetching podcast episodes'
},
feedUrl: {
title: 'Request failed',
details: 'Error while fetching feed url'
}
}
}
},
select: {
Expand Down
35 changes: 35 additions & 0 deletions client/src/lib/persist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { pick } from 'lodash-es';
import type { State } from '../store';
import * as localStorage from '../lib/local-storage';

const persistKey = (): string | null => {
try {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('site_url');
} catch (err) {
return null;
}
};

export const persistState = (state: State) => {
const key = persistKey();

if (!key) {
return;
}

localStorage.save(
key,
pick(state, ['authentication', 'podcast', 'onboarding', 'feed', 'episodes'])
);
};

export const getPersistedState= (): Partial<State> | undefined => {
const key = persistKey();

if (!key) {
return undefined;
}

return localStorage.get<Partial<State>>(key) || undefined;
}
8 changes: 5 additions & 3 deletions client/src/lib/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ const headers = (): HeadersInit => {
...(site ? {'Wordpress-Site': site}: {})
};
};
const checkResponse = (response: Response): Response => {
const checkResponse = async (response: Response): Promise<Response> => {
if (!response.ok) {
throw new Error('API call failed!')
const error = await response.text();
throw new Error(error);
}

return response;
}

const parseResponse = <T>(response: Response): Promise<T> => { return response.json(); }
const parseResponse = <T>(response: Response): Promise<T> => response.json();

export const origin = (path: string): string => {
const url = new URL(document.baseURI).origin;
Expand Down
6 changes: 5 additions & 1 deletion client/src/pages/onboarding/Onboarding.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<template>
<div ref="viewPort" class="relative h-full flex flex-col" :data-test="`step-${state.current.name}`">
<Steps class="fixed top-0 w-full z-50" :steps="state.steps" v-if="stepsVisible"></Steps>
<NotificationsFeature :class="{ 'mt-[80px]': stepsVisible }" />
<div
id="content"
class="pt-4 pb-[50px] sm:px-6 xl:pl-6 overflow-y-auto"
class="pt-4 pb-[50px] sm:px-6 xl:pl-6 overflow-y-auto relative"
:class="{ 'pt-[80px]': stepsVisible }"

>
<about />
<component :is="stepComponents[state.current.name]" />
</div>
<div class="fixed bottom-0 flex justify-between w-full px-4 sm:px-6 xl:pl-6 py-2 bg-white z-50">
Expand All @@ -32,7 +34,9 @@ import { useI18n } from 'vue-i18n';

import { selectors } from '../../store';
import Steps from './components/Steps.vue';
import About from './components/About.vue';
import PodloveButton from '../../components/button/Button.vue';
import NotificationsFeature from '../../features/notifications/Notifications.vue';

import SetupType from './steps/SetupType.vue';

Expand Down
Loading
Loading