mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
refactor: improved CTAs on callbackfeedback modal
This commit is contained in:
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user