refactor: improved CTAs on callbackfeedback modal

This commit is contained in:
Zack Spear
2023-06-30 21:09:23 -07:00
parent a39f879b3a
commit 747bacb901
10 changed files with 158 additions and 114 deletions

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { XCircleIcon } from '@heroicons/vue/24/solid';
export interface Props {
btnStyle?: 'fill' | 'outline';
btnStyle?: 'fill' | 'outline' | 'underline';
btnType?: 'button' | 'submit' | 'reset';
download?: boolean;
external?: boolean;
href?: string;
@@ -10,6 +11,7 @@ export interface Props {
}
const props = withDefaults(defineProps<Props>(), {
btnStyle: 'fill',
btnType: 'button',
});
defineEmits(['click']);
@@ -19,7 +21,9 @@ const classes = computed(() => {
case 'fill':
return 'text-white bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60';
case 'outline':
return 'text-orange-dark bg-gradient-to-r from-transparent to-transparent border border-solid border-orange-dark hover:text-white focus:text-white hover:from-unraid-red hover:to-orange focus:from-unraid-red focus:to-orange hover:border-transparent focus:border-transparent';
return 'text-orange bg-gradient-to-r from-transparent to-transparent border border-solid border-orange hover:text-white focus:text-white hover:from-unraid-red hover:to-orange focus:from-unraid-red focus:to-orange hover:border-transparent focus:border-transparent';
case 'underline':
return 'opacity-75 hover:opacity-100 focus:opacity-100 underline transition hover:text-alpha hover:bg-beta focus:text-alpha focus:bg-beta';
}
});
</script>
@@ -31,6 +35,7 @@ const classes = computed(() => {
:href="href"
:rel="external ? 'noopener noreferrer' : ''"
:target="external ? '_blank' : ''"
:type="!href ? btnType : ''"
class="text-14px text-center font-semibold flex-none flex flex-row items-center justify-center gap-x-8px px-8px py-8px cursor-pointer rounded-md"
:class="classes"
>

View File

@@ -17,16 +17,12 @@ const downloadUrl = computed(() => new URL(`/graphql/api/logs?apiKey=${apiKey.va
The primary method of support for Unraid Connect is through <a href="https://forums.unraid.net/forum/94-connect-plugin-support/" target="_blank" rel="noopener noreferrer">our forums</a> and <a href="https://discord.gg/unraid" target="_blank" rel="noopener noreferrer">Discord</a>. If you are asked to supply logs, please open a support request on our <a href="https://unraid.net/contact" target="_blank" rel="noopener noreferrer">Contact Page</a> and reply to the email message you receive with your logs attached. The logs may contain sensitive information so do not post them publicly.
</span>
<span>
<a
<BrandButton
:href="downloadUrl.toString()"
rel="noopener noreferrer"
class="text-white text-14px text-center w-full flex-none flex flex-row items-center justify-center gap-x-8px px-8px py-8px cursor-pointer rounded-md bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60"
target="_blank"
download
>
<ArrowDownTrayIcon class="flex-shrink-0 w-14px" />
{{ 'Download' }}
</a>
:external="true"
:icon="ArrowDownTrayIcon"
:text="'Download'" />
</span>
</div>
</template>

View File

@@ -73,7 +73,7 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
success ? 'shadow-green-600/30 border-green-600/10' : '',
!error && !success ? 'shadow-orange/10 border-white/10' : '',
]"
class="text-beta bg-alpha border-2 border-solid relative transform overflow-hidden rounded-lg px-4 pb-4 pt-5 sm:my-8 sm:p-6 text-left shadow-xl transition-all sm:w-full"
class="text-16px text-beta bg-alpha text-left relative flex flex-col justify-around p-16px my-24px sm:p-24px border-2 border-solid shadow-xl transform overflow-hidden rounded-lg transition-all sm:w-full"
>
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 hidden pt-2 pr-2 sm:block">
<button @click="closeModal" type="button" class="rounded-md text-beta bg-alpha p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
@@ -94,7 +94,7 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
</header>
<slot name="main"></slot>
<footer class="text-14px relative -mx-4px -mb-4px sm:-mx-6 sm:-mb-6 p-4 sm:p-6">
<footer class="text-14px relative -mx-16px -mb-16px sm:-mx-24px sm:-mb-24px p-4 sm:p-6">
<div class="absolute z-0 inset-0 opacity-10 bg-beta"></div>
<div class="relative z-10">
<slot name="footer"></slot>

View File

@@ -1,9 +1,10 @@
<script lang="ts" setup>
import { useClipboard } from '@vueuse/core'
import { ClipboardIcon, InformationCircleIcon } from '@heroicons/vue/24/solid';
import { ClipboardIcon, CogIcon, InformationCircleIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { PLUGIN_SETTINGS } from '~/helpers/urls';
import { useAccountStore } from '~/store/account';
import { useCallbackActionsStore } from '~/store/callbackActions';
import { useInstallKeyStore } from '~/store/installKey';
@@ -25,8 +26,11 @@ const promoStore = usePromoStore();
const serverStore = useServerStore();
const {
accountAction,
accountActionHide,
accountActionStatus,
accountActionStatusCopy,
accountActionType,
} = storeToRefs(accountStore);
const {
callbackStatus,
@@ -40,12 +44,27 @@ const {
} = storeToRefs(installKeyStore);
const {
pluginInstalled,
registered,
authAction,
} = storeToRefs(serverStore);
/** @todo if post purchase/upgrade thank user for their purchase and support */
/** @todo if post purchase/upgrade and no Connect, show CTA to Connect promo */
/** @todo if signing in show CTA to head to Connect settings to enable features */
/**
* Post sign in success state:
* If we're on the Connect settings page in the webGUI
* the modal should close instead of redirecting to the
* settings page.
*
* @todo figure out the difference between document.location and window.location
*/
const isSettingsPage = ref<boolean>(document.location.pathname === '/Settings/ManagementAccess');
const showPromoCta = computed(() => callbackStatus.value === 'success' && !pluginInstalled.value);
const showSignInCta = computed(() => pluginInstalled.value && !registered.value && authAction.value?.name === 'signIn' && accountActionType.value !== 'signIn');
const heading = computed(() => {
switch (callbackStatus.value) {
case 'error':
@@ -57,12 +76,17 @@ const heading = computed(() => {
}
});
const subheading = computed(() => {
switch (callbackStatus.value) {
case 'loading':
return 'Please keep this window open while we perform some actions';
default:
return '';
if (callbackStatus.value === 'error') {
return 'Something went wrong'; /** @todo show actual error messages */
}
if (callbackStatus.value === 'loading') return 'Please keep this window open while we perform some actions';
if (callbackStatus.value === 'success') {
if (accountActionType.value === 'signIn') return `You're one step closer to enhancing your Unraid experience`;
if (keyActionType.value === 'purchase') return `Thank you for purchasing an Unraid ${keyType.value} Key!`;
if (keyActionType.value === 'upgrade') return `Thank you for upgrading to an Unraid ${keyType.value} Key!`;
return '';
}
return '';
});
const close = () => {
@@ -91,59 +115,81 @@ const { text, copy, copied, isSupported } = useClipboard({ source: keyUrl.value
:show-close-x="callbackStatus !== 'loading'"
>
<template #main>
<div class="text-16px text-center relative w-full min-h-[15vh] flex flex-col justify-center gap-y-16px">
<div
v-if="keyInstallStatus !== 'ready' || accountActionStatus !== 'ready'"
class="flex flex-col gap-y-16px"
<div
v-if="keyInstallStatus !== 'ready' || accountActionStatus !== 'ready'"
class="text-center relative w-full min-h-[15vh] flex flex-col justify-center gap-y-16px py-16px"
>
<UpcCallbackFeedbackStatus
v-if="keyInstallStatus !== 'ready'"
:success="keyInstallStatus === 'success'"
:error="keyInstallStatus === 'failed'"
:text="keyInstallStatusCopy.text"
>
<UpcCallbackFeedbackStatus
v-if="keyInstallStatus !== 'ready'"
:success="keyInstallStatus === 'success'"
:error="keyInstallStatus === 'failed'"
:text="keyInstallStatusCopy.text"
>
<UpcUptimeExpire v-if="keyType === 'Trial'" :for-expire="true" class="opacity-75 italic mt-4px" />
<UpcUptimeExpire v-if="keyType === 'Trial'" :for-expire="true" class="opacity-75 italic mt-4px" />
<template v-if="keyInstallStatus === 'failed'">
<div v-if="isSupported" class="flex justify-center">
<BrandButton
@click="copy(keyUrl)"
:icon="ClipboardIcon"
:text="copied ? 'Copied' : 'Copy Key URL'" />
</div>
<p v-else>{{ 'Copy your Key URL' }}: {{ keyUrl }}</p>
<p><a href="/Tools/Registration" class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition">{{ 'Then go to Tools > Registration to manually install it' }}</a></p>
</template>
</UpcCallbackFeedbackStatus>
<template v-if="keyInstallStatus === 'failed'">
<div v-if="isSupported" class="flex justify-center">
<BrandButton
@click="copy(keyUrl)"
:icon="ClipboardIcon"
:text="copied ? 'Copied' : 'Copy Key URL'" />
</div>
<p v-else>{{ 'Copy your Key URL' }}: {{ keyUrl }}</p>
<p><a href="/Tools/Registration" class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition">{{ 'Then go to Tools > Registration to manually install it' }}</a></p>
</template>
</UpcCallbackFeedbackStatus>
<UpcCallbackFeedbackStatus
v-if="accountActionStatus !== 'ready'"
:success="accountActionStatus === 'success'"
:error="accountActionStatus === 'failed'"
:text="accountActionStatusCopy.text" />
</div>
<UpcCallbackFeedbackStatus
v-if="accountActionStatus !== 'ready' && !accountActionHide"
:success="accountActionStatus === 'success'"
:error="accountActionStatus === 'failed'"
:text="accountActionStatusCopy.text" />
<UpcCallbackFeedbackStatus
v-if="showPromoCta"
:icon="InformationCircleIcon"
:text="'Enhance your Unraid experience with Unraid Connect'" />
<UpcCallbackFeedbackStatus
v-if="showSignInCta"
:icon="InformationCircleIcon"
:text="'Sign In to utilize Unraid Connect'" />
</div>
</template>
<template #footer>
<div v-if="callbackStatus === 'success'" class="flex flex-col gap-y-16px">
<div v-if="!pluginInstalled" class="text-center flex flex-col justify-center gap-y-8px">
<p>{{ 'Enhance your Unraid experience with Unraid Connect' }}</p>
<span class="inline-flex justify-center">
<BrandButton
@click="promoClick"
:icon="InformationCircleIcon"
:text="'Learn More'"
/>
</span>
</div>
<div v-if="callbackStatus === 'success'" class="flex flex-row-reverse justify-center gap-16px">
<template v-if="pluginInstalled && accountActionType === 'signIn'">
<BrandButton
v-if="isSettingsPage"
@click="close"
:icon="CogIcon"
:text="'Configure Connect Features'"
class="grow-0" />
<BrandButton
v-else
:href="PLUGIN_SETTINGS"
:icon="CogIcon"
:text="'Configure Connect Features'"
class="grow-0" />
</template>
<button
<BrandButton
v-if="showPromoCta"
@click="promoClick"
:text="'Learn More'" />
<BrandButton
v-if="showSignInCta"
@click="authAction?.click"
:external="authAction?.external"
:icon="authAction?.icon"
:text="authAction?.text" />
<BrandButton
@click="close"
class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition"
>
{{ !pluginInstalled ? 'No Thanks' : 'Close' }}
</button>
btn-style="underline"
:text="!pluginInstalled ? 'No Thanks' : 'Close'" />
</div>
</template>
</Modal>

View File

@@ -3,6 +3,7 @@ import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/solid';
export interface Props {
error?: boolean;
icon?: typeof CheckCircleIcon;
success?: boolean;
text?: string;
}
@@ -14,10 +15,11 @@ withDefaults(defineProps<Props>(), {
</script>
<template>
<div>
<div class="mx-auto max-w-[45ch]">
<div class="flex items-center justify-center gap-x-8px">
<CheckCircleIcon v-if="success" class="fill-green-600 w-24px" />
<XCircleIcon v-if="error" class="fill-unraid-red w-24px" />
<CheckCircleIcon v-if="success" class="fill-green-600 w-24px shrink-0" />
<XCircleIcon v-if="error" class="fill-unraid-red w-24px shrink-0" />
<component v-if="icon" :is="icon" class="fill-current opacity-75 w-24px shrink-0" />
<p>{{ text }}</p>
</div>
<slot></slot>

View File

@@ -14,8 +14,8 @@ const showExpireTime = computed(() => {
<template>
<div class="flex flex-col gap-y-24px w-full min-w-300px md:min-w-[500px] max-w-4xl p-16px">
<header class="text-center">
<h2 class="text-24px font-semibold">Thank you for installing Connect!</h2>
<h3>Sign In to your Unraid.net account to register your server</h3>
<h2 class="text-24px font-semibold">{{ 'Thank you for installing Connect!' }}</h2>
<h3>{{ 'Sign In to your Unraid.net account to get started' }}</h3>
<UpcUptimeExpire v-if="showExpireTime" class="opacity-75 mt-12px" />
</header>
<ul class="list-reset flex flex-col gap-y-8px px-16px" v-if="stateData.actions">

View File

@@ -75,7 +75,7 @@ const installButtonClasses = 'text-white text-14px text-center w-full flex flex-
<template #main>
<div class="text-center relative w-full">
<div class="flex flex-wrap justify-center my-16px md:my-24px">
<div class="grid grid-cols-1 sm:grid-cols-2 justify-center p-16px md:py-24px gap-16px">
<UpcPromoFeature
v-for="(feature, index) in features"
:key="index"

View File

@@ -10,13 +10,7 @@ defineProps<Props>();
</script>
<template>
<div
class="text-left relative flex overflow-hidden p-8px"
:class="{
'w-full sm:w-1/2': !center,
'max-w-640px': center,
}"
>
<div class="text-left relative flex overflow-hidden">
<span v-if="!center" class="flex-shrink-0">
<slot></slot>
</span>

View File

@@ -16,10 +16,40 @@ export const useAccountStore = defineStore('account', () => {
// State
const accountAction = ref<ExternalSignIn|ExternalSignOut>();
const accountActionHide = ref<boolean>(false);
const accountActionStatus = ref<'failed' | 'ready' | 'success' | 'updating'>('ready');
const username = ref<string>('');
// Getters
const accountActionType = computed(() => accountAction.value?.type);
const accountActionStatusCopy = computed((): { text: string; } => {
switch (accountActionStatus.value) {
case 'ready':
return {
text: 'Ready to update Connect account configuration',
};
case 'updating':
return {
text: accountAction.value?.type === 'signIn'
? `Signing in ${accountAction.value.user?.preferred_username}...`
: `Signing out ${username.value}...`,
};
case 'success':
return {
text: accountAction.value?.type === 'signIn'
? `${accountAction.value.user?.preferred_username} Signed In Successfully`
: `${username.value} Signed Out Successfully`,
};
case 'failed':
return {
text: accountAction.value?.type === 'signIn'
? 'Sign In Failed'
: 'Sign Out Failed',
};
}
});
// Actions
const recover = () => {
console.debug('[accountStore.recover]');
@@ -98,6 +128,13 @@ export const useAccountStore = defineStore('account', () => {
}),
};
if (!serverStore.registered && !accountAction.value.user) {
console.debug('[accountStore.updatePluginConfig] Not registered skipping sign out');
accountActionHide.value = true;
accountActionStatus.value = 'success';
return;
}
try {
const response = await WebguiUpdate
.formUrl({
@@ -122,43 +159,14 @@ export const useAccountStore = defineStore('account', () => {
}
};
const accountActionStatusCopy = computed((): { text: string; } => {
switch (accountActionStatus.value) {
case 'ready':
return {
text: 'Ready to update Connect account configuration',
};
case 'updating':
return {
text: accountAction.value?.type === 'signIn'
? `Signing in ${accountAction.value.user?.preferred_username}...`
: `Signing out ${username.value}...`,
};
case 'success':
return {
text: accountAction.value?.type === 'signIn'
? `${accountAction.value.user?.preferred_username} Signed In Successfully`
: `${username.value} Signed Out Successfully`,
};
case 'failed':
return {
text: accountAction.value?.type === 'signIn'
? 'Sign In Failed'
: 'Sign Out Failed',
};
}
});
watch(accountActionStatus, (newV, oldV) => {
console.debug('[accountActionStatus.watch]', newV, oldV);
});
return {
// State
accountAction,
accountActionHide,
accountActionStatus,
// Getters
accountActionStatusCopy,
accountActionType,
// Actions
recover,
replace,

View File

@@ -70,22 +70,15 @@ export const useCallbackActionsStore = defineStore(
watch(callbackStatus, (newVal, _oldVal) => {
console.debug('[callbackStatus]', newVal);
if (newVal === 'ready') {
console.debug('[callbackStatus]', newVal, 'addEventListener');
window.addEventListener('beforeunload', preventClose);
}
// removing query string once actions are done so users can't refresh the page and go through the same actions
if (newVal !== 'ready') {
console.debug('[callbackStatus]', newVal, 'removeEventListener');
window.removeEventListener('beforeunload', preventClose);
console.debug('[callbackStatus] replace history w/o query');
// removing query string once actions are done so users can't refresh the page and go through the same actions
window.history.replaceState(null, '', window.location.pathname);
}
});
watch(callbackData, () => {
console.debug('[callbackData] watch', callbackData.value);
});
return {
redirectToCallbackType,
callbackData,