diff --git a/public/assets/icon-confirm.svg b/public/assets/icon-confirm.svg
new file mode 100644
index 00000000..b4e6e1f6
--- /dev/null
+++ b/public/assets/icon-confirm.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/assets/icon-external.svg b/public/assets/icon-external.svg
new file mode 100644
index 00000000..22e79c95
--- /dev/null
+++ b/public/assets/icon-external.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/assets/nudge/icon-circle-dismiss.svg b/public/assets/nudge/icon-circle-dismiss.svg
new file mode 100644
index 00000000..6ea3e714
--- /dev/null
+++ b/public/assets/nudge/icon-circle-dismiss.svg
@@ -0,0 +1 @@
+
diff --git a/src/lib/app-context/profile-completion.model.ts b/src/lib/app-context/profile-completion.model.ts
index 26153bc9..dd2d0640 100644
--- a/src/lib/app-context/profile-completion.model.ts
+++ b/src/lib/app-context/profile-completion.model.ts
@@ -3,4 +3,5 @@ export interface ProfileCompletionData {
handle: string
percentComplete: number
showToast: string
+ dateFields?: [string, Date][]
}
diff --git a/src/lib/components/modals/Modal.module.scss b/src/lib/components/modals/Modal.module.scss
index da96bdec..aaee3629 100644
--- a/src/lib/components/modals/Modal.module.scss
+++ b/src/lib/components/modals/Modal.module.scss
@@ -55,7 +55,7 @@
border: 1px solid;
border-radius: 1px;
color: #e9e9e9;
- margin: 24px 0;
+ margin: 12px 0;
width: 100%;
display: block;
}
diff --git a/src/lib/components/sticky/Sticky.svelte b/src/lib/components/sticky/Sticky.svelte
index 4b9f2be9..9e023b82 100644
--- a/src/lib/components/sticky/Sticky.svelte
+++ b/src/lib/components/sticky/Sticky.svelte
@@ -5,8 +5,10 @@
let className = '';
export { className as class };
export let yOffset = 0;
+ export let delayYOffset = 0;
let elRef: HTMLElement | undefined;
+ let placeholderRef: HTMLElement | undefined;
let elYOffset = 0;
function handleScroll() {
@@ -15,7 +17,10 @@
}
const { scrollY } = window;
- const isFixed = (scrollY + yOffset - elYOffset) >= 0;
+ const isFixed = (scrollY + yOffset - elYOffset - delayYOffset) >= 0;
+ if (placeholderRef) {
+ Object.assign(placeholderRef.style, {height: isFixed ? `${elRef.offsetHeight}px` : 0});
+ }
elRef.classList.toggle(styles.sticky, isFixed);
}
@@ -31,6 +36,7 @@
})
+
diff --git a/src/lib/components/user-area/UserArea.svelte b/src/lib/components/user-area/UserArea.svelte
index fd9a4070..e0cbd830 100644
--- a/src/lib/components/user-area/UserArea.svelte
+++ b/src/lib/components/user-area/UserArea.svelte
@@ -39,31 +39,20 @@
debounce = user.handle;
- if (!DISABLE_NUDGES) {
- const completednessData = await fetchUserProfileCompletedness(user, true);
- if (!completednessData) {
- return;
- }
- $ctx.auth = {
- ...$ctx.auth,
- profileCompletionData: {
- completed: completednessData.data?.percentComplete === 100,
- handle: completednessData.handle,
- percentComplete: completednessData.data?.percentComplete,
- showToast: completednessData.showToast,
- },
- };
- } else {
- $ctx.auth = {
- ...$ctx.auth,
- profileCompletionData: {
- completed: true,
- handle: user?.handle,
- percentComplete: 0,
- showToast: "",
- },
- };
+ const completednessData = await fetchUserProfileCompletedness(user, true);
+ if (!completednessData) {
+ return;
}
+ $ctx.auth = {
+ ...$ctx.auth,
+ profileCompletionData: {
+ completed: completednessData.data?.percentComplete === 100,
+ handle: completednessData.handle,
+ percentComplete: completednessData.data?.percentComplete,
+ showToast: DISABLE_NUDGES ? '' : completednessData.showToast,
+ dateFields: completednessData.data?.dateFields,
+ },
+ };
setTimeout(() => debounce = '', 100);
}
diff --git a/src/lib/functions/profile-nudges.ts b/src/lib/functions/profile-nudges.ts
index b0361bf3..3129c20f 100644
--- a/src/lib/functions/profile-nudges.ts
+++ b/src/lib/functions/profile-nudges.ts
@@ -4,17 +4,18 @@ import { DISABLE_NUDGES, NUDGES_DISABLED_HOSTS } from "lib/config/profile-toasts
import { getRequestAuthHeaders } from "./auth-jwt";
+export function isOnHost(host: string): boolean {
+ const locationHostname = window?.location.hostname ?? ''
+ return !!host.match(new RegExp(`^https?:\/\/${locationHostname}`, 'i'));
+}
+
/**
* Check if we're on a domain that should not show the profile nudges
* @returns Boolean
*/
-export function dismissNudgesBasedOnHost(): boolean {
+export function dismissNudgesBasedOnHost(exceptHost?: string): boolean {
// Ue the new flag to disable the profile nudges completely (PS-267)
- const locationHostname = window?.location.hostname ?? ''
- return DISABLE_NUDGES ||
- (!!NUDGES_DISABLED_HOSTS.find(host => (
- host.match(new RegExp(`^https?:\/\/${locationHostname}`, 'i'))
- )));
+ return DISABLE_NUDGES || NUDGES_DISABLED_HOSTS.filter(h => !exceptHost || h !== exceptHost).some(isOnHost);
}
// store fetched data in a local cache (de-duplicate immediate api calls)
@@ -25,7 +26,7 @@ export interface ProfileCompletednessResponse {
showToast: string
data: {
percentComplete: number
- }
+ } & {[key: string]: any}
}
/**
@@ -55,13 +56,29 @@ export const fetchUserProfileCompletedness = async (user: AuthUser, force = fals
const request = fetch(requestUrl, {headers: {...getRequestAuthHeaders()}});
const response = await (await request).json();
+
+ const responseData = response.data ?? {};
+ const dateFields = Object.keys(responseData)
+ .filter(k => k.endsWith('LastUpdateDate') || k === 'lastProfileConfirmationDate')
+ .map((key) => [key, new Date(responseData[key])]);
+
resolve({
...response,
data: {
- ...response.data,
+ ...responseData,
+ dateFields,
percentComplete: (response?.data?.percentComplete ?? 0) * 100,
},
});
return localCache[cacheKey];
}
+
+export const confirmProfileData = async (userHandle: string) => {
+ const requestUrl: string = `${TC_API_HOST}/members/${userHandle}/confirmProfile`;
+ const request = fetch(requestUrl, {method: 'POST', headers: {...getRequestAuthHeaders()}});
+
+ const response = await (await request).json();
+
+ return response.data;
+}
diff --git a/src/lib/nudge-app/Nudge.module.scss b/src/lib/nudge-app/Nudge.module.scss
index 12c2a90b..24759207 100644
--- a/src/lib/nudge-app/Nudge.module.scss
+++ b/src/lib/nudge-app/Nudge.module.scss
@@ -1,5 +1,6 @@
@use 'lib/styles/mixins.scss' as *;
+.bannerOuter,
.nudgeOuter {
position: absolute;
top: calc(var(--uninav-header-height) + var(--top-offset));
@@ -13,6 +14,12 @@
}
}
+.bannerOuter {
+ position: relative;
+ top: 0;
+ height: min-content;
+}
+
.nudgeWrap {
position: absolute;
display: flex;
@@ -41,3 +48,26 @@
width: 100%;
}
}
+
+.bannerWrap {
+ display: flex;
+ width: 100%;
+ background: #60267d;
+ color: #fff;
+}
+
+.bannerInner {
+ width: 100%;
+ margin: 0 auto;
+ @include maxViewWidth;
+ padding: 6px 32px;
+
+ @include tablet {
+ padding: 6px 16px;
+ }
+
+ @include mobile {
+ padding: 0 2px;
+ width: 100%;
+ }
+}
diff --git a/src/lib/nudge-app/Nudge.svelte b/src/lib/nudge-app/Nudge.svelte
index ffc95d71..1cda4807 100644
--- a/src/lib/nudge-app/Nudge.svelte
+++ b/src/lib/nudge-app/Nudge.svelte
@@ -2,10 +2,12 @@
import { getAppContext } from 'lib/app-context';
import Toastr from './components/Toastr.svelte';
import styles from './Nudge.module.scss';
- import type { ToastType } from 'lib/config/profile-toasts.config';
+ import { toastsMeta, type ToastType } from 'lib/config/profile-toasts.config';
import { getToast, hideToast } from './toast-manager';
import Sticky from 'lib/components/sticky/Sticky.svelte';
import type { ProfileCompletionData } from 'lib/app-context/profile-completion.model';
+ import { getBanner, hide as hideBanner } from './banner-manager';
+ import Banner from './components/Banner.svelte';
const ctx = getAppContext();
@@ -16,20 +18,42 @@
} = $ctx.auth)
let toast: ToastType | undefined;
+ let banner: {key: string, date: Date} | undefined;
function dismissToast() {
toast = undefined;
hideToast();
}
+ function dismissBanner() {
+ banner = undefined;
+ hideBanner();
+ }
+
$: toast = isReady ? getToast(profileCompletionData as ProfileCompletionData) : undefined;
+ $: banner = isReady ? getBanner(profileCompletionData as ProfileCompletionData) : undefined;
+{#if banner}
+
+
+
+{/if}
+
{#if toast}
diff --git a/src/lib/nudge-app/banner-manager.ts b/src/lib/nudge-app/banner-manager.ts
new file mode 100644
index 00000000..a980596d
--- /dev/null
+++ b/src/lib/nudge-app/banner-manager.ts
@@ -0,0 +1,72 @@
+import type { ProfileCompletionData } from 'lib/app-context/profile-completion.model';
+import { checkCookie, getCookieValue, setCookie } from '../utils/cookies';
+import { dismissNudgesBasedOnHost } from '../functions/profile-nudges';
+import { PROFILE_HOST } from 'lib/config';
+
+const COOKIE_NAME = 'uni-profilereminder-banner-shown';
+const COOKIE_ACTIVE_PERIOD_DAYS = 3;
+const PROFILE_UPDATE_REMINDER_PERIOD_DAYS = 3*30;
+
+const DAY = 24 * 3600 * 1000;
+const _30DAYS = 30 * DAY;
+
+function isDismissed() {
+ return checkCookie(COOKIE_NAME, 'hidden');
+}
+
+function getLastSeen() {
+ return getCookieValue(COOKIE_NAME);
+}
+
+const isOlderThanTreshold = (date: Date | number, treshold: number): boolean => {
+ const diffInDays = Math.floor((Date.now() - +date) / DAY);
+ return diffInDays >= treshold;
+}
+
+/**
+ * @param completednessData
+ * @returns
+ */
+export const getBanner = (completednessData: ProfileCompletionData) => {
+ if (dismissNudgesBasedOnHost(PROFILE_HOST) || !completednessData || isDismissed()) {
+ return;
+ }
+
+ const fields = completednessData.dateFields ?? [];
+ const updatableProfileFields: [string, Date][] = fields
+ .filter(([k]) => k.endsWith('LastUpdateDate'))
+ .map(([k, d]) => [k.replace(/LastUpdateDate$/, ''), d]);
+ const lastProfileConfirmationDate = fields.find(([k]) => k === 'lastProfileConfirmationDate')?.[1];
+
+ const sorted = updatableProfileFields
+ .sort((a, b) => +a[1] - +b[1]);
+
+
+ const lastUpdate = +sorted[sorted.length - 1][1];
+ if (!lastUpdate) {
+ return;
+ }
+
+ const lastUpdateOrCofirmDate = lastProfileConfirmationDate ? Math.max(lastUpdate, +lastProfileConfirmationDate) : lastUpdate;
+ if (!lastUpdateOrCofirmDate || !isOlderThanTreshold(lastUpdateOrCofirmDate, PROFILE_UPDATE_REMINDER_PERIOD_DAYS)) {
+ setCookie(COOKIE_NAME, '', 0);
+ return;
+ }
+
+ let lastSeen = getLastSeen();
+ if (!lastSeen) {
+ const oldFields = updatableProfileFields.filter(([,d]) => isOlderThanTreshold(d, PROFILE_UPDATE_REMINDER_PERIOD_DAYS)).map(([k]) => k);
+ const fieldKey = oldFields.length ? oldFields[0] : undefined;
+ const field = updatableProfileFields.find(f => f[0] === fieldKey) ?? [];
+ lastSeen = JSON.stringify({key: field[0], date: field[1]});
+ if(lastSeen) {
+ setCookie(COOKIE_NAME, lastSeen, COOKIE_ACTIVE_PERIOD_DAYS);
+ }
+ }
+
+ return JSON.parse(lastSeen);
+}
+
+export const hide = () => {
+ setCookie(COOKIE_NAME, 'hidden', COOKIE_ACTIVE_PERIOD_DAYS);
+}
diff --git a/src/lib/nudge-app/components/Banner.module.scss b/src/lib/nudge-app/components/Banner.module.scss
new file mode 100644
index 00000000..ebc40df1
--- /dev/null
+++ b/src/lib/nudge-app/components/Banner.module.scss
@@ -0,0 +1,126 @@
+@use 'lib/styles/fonts.scss' as *;
+@use 'lib/styles/palette.scss' as *;
+@use 'lib/styles/mixins.scss' as *;
+$btnSize: 20px;
+
+.banner {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: $nunito;
+ font-size: 14px;
+ line-height: 20px;
+
+ &:global(.hidden) {
+ opacity: 0;
+ // visibility: hidden;
+ pointer-events: none;
+ // move up and fade in when shown
+ transform: translate(0, -30px);
+ transition: transform 0.3s ease-in-out, opacity 0.18s ease-in-out 0.1s, visibility 0.01ms 0.3s;
+ }
+
+ &:not(:global(.hidden)) {
+ opacity: 1;
+ // visibility: visible;
+ pointer-events: all;
+ // move down and fade out when hidden
+ transform: translate(0, 0);
+ transition: transform 0.3s ease-in-out, opacity 0.18s ease-in-out, visibility 0.01ms;
+ }
+
+ @include tablet {
+ flex-wrap: wrap;
+ padding: 4px 12px 8px;
+ .spacer {
+ width: 100%;
+ height: 0;
+ margin-top: 6px;
+ }
+ }
+}
+
+.contents {
+ flex: 1 1 auto;
+ text-align: center;
+ justify-content: center;
+
+ &, a {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ a {
+ &:hover {
+ text-decoration: underline !important;
+ }
+ }
+}
+
+.close {
+ flex: 0 0 16px;
+ cursor: pointer;
+}
+
+.externalIcon {
+ margin-top: -2px;
+ width: 16px;
+ height: 16px;
+ display: block;
+}
+
+
+.inlineBtn {
+ height: $btnSize;
+ cursor: pointer;
+ font-size: 16px;
+ font-family: $roboto;
+ border: 0;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ background: transparent;
+ padding: 0;
+ border-radius: 0.25rem;
+
+ transition: background 0.15s ease;
+ cursor: pointer;
+ text-decoration: underline;
+ &:hover {
+ background: rgb(122 74 146);
+ }
+ &:active {
+ background: rgb(69 27 90);
+ }
+}
+
+.confirmBtn {
+ cursor: pointer;
+ font-family: $roboto;
+ border: 1px solid #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ color: #fff;
+ background: transparent;
+ padding: 2px 8px;
+ margin: -2px 0;
+ margin-right: 8px;
+ border-radius: 0.25rem;
+
+ transition: background 0.15s ease;
+ &:hover {
+ background: rgb(122 74 146);
+ }
+ &:active {
+ background: rgb(69 27 90);
+ }
+}
+
+.modalBtns {
+ margin-top: 24px;
+}
diff --git a/src/lib/nudge-app/components/Banner.svelte b/src/lib/nudge-app/components/Banner.svelte
new file mode 100644
index 00000000..7379f9aa
--- /dev/null
+++ b/src/lib/nudge-app/components/Banner.svelte
@@ -0,0 +1,131 @@
+
+
+
+ {#if isOnProfiles}
+
+ If your profile is up to date, you can
+
+ to confirm.
+
+ {:else}
+
+
+
+
handleCloseKeydown(ev, true)}
+ >
+ Update
+
+
+
+ {/if}
+
+
+

+
+
+
+
+ Please make sure your profile is up to date.
+
+
+
+
diff --git a/src/lib/styles/main.scss b/src/lib/styles/main.scss
index 9cade287..7493c41b 100644
--- a/src/lib/styles/main.scss
+++ b/src/lib/styles/main.scss
@@ -1,5 +1,5 @@
html {
- --uninav-header-height: 60px;
+ --uninav-header-height: 58px;
@media (max-width: 767px) {
--uninav-header-height: 48px;
diff --git a/src/lib/tool-navigation/tool-nav-separator/ToolNavSeparator.module.scss b/src/lib/tool-navigation/tool-nav-separator/ToolNavSeparator.module.scss
index 050c1ec8..6c2fb4b4 100644
--- a/src/lib/tool-navigation/tool-nav-separator/ToolNavSeparator.module.scss
+++ b/src/lib/tool-navigation/tool-nav-separator/ToolNavSeparator.module.scss
@@ -2,7 +2,7 @@
position: relative;
background: #D4D4D4;
width: 1px;
- height: 60px;
+ height: var(--uninav-header-height);
margin: -18px 0;
margin-left: 0;
z-index: 1;
diff --git a/src/main.ts b/src/main.ts
index 32e4659d..2aa50223 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -129,6 +129,9 @@ async function init(
instancesContextStore[targetId] = ctx;
+ if (navType === 'tool' || navType === 'marketing') {
+ await loadNudgeApp(ctx, targetEl as Element);
+ }
// load the navigation component
const Navigation = await loadNavigationFn();
// instantiate the navigation component
@@ -138,10 +141,6 @@ async function init(
if (typeof readyCallback === 'function') {
readyCallback();
}
-
- if (navType === 'tool' || navType === 'marketing') {
- loadNudgeApp(ctx, targetEl.querySelector('.tc-universal-nav-wrap') as Element);
- }
}
/**
diff --git a/types/src/lib/app-context/profile-completion.model.d.ts b/types/src/lib/app-context/profile-completion.model.d.ts
index f1de542b..b3062f0c 100644
--- a/types/src/lib/app-context/profile-completion.model.d.ts
+++ b/types/src/lib/app-context/profile-completion.model.d.ts
@@ -3,4 +3,5 @@ export interface ProfileCompletionData {
handle: string;
percentComplete: number;
showToast: string;
+ dateFields?: [string, Date][];
}
diff --git a/types/src/lib/functions/profile-nudges.d.ts b/types/src/lib/functions/profile-nudges.d.ts
index 092da7a5..d0905b4c 100644
--- a/types/src/lib/functions/profile-nudges.d.ts
+++ b/types/src/lib/functions/profile-nudges.d.ts
@@ -1,14 +1,17 @@
import type { AuthUser } from "lib/app-context";
+export declare function isOnHost(host: string): boolean;
/**
* Check if we're on a domain that should not show the profile nudges
* @returns Boolean
*/
-export declare function dismissNudgesBasedOnHost(): boolean;
+export declare function dismissNudgesBasedOnHost(exceptHost?: string): boolean;
export interface ProfileCompletednessResponse {
handle: string;
showToast: string;
data: {
percentComplete: number;
+ } & {
+ [key: string]: any;
};
}
/**
@@ -16,3 +19,4 @@ export interface ProfileCompletednessResponse {
* @returns Promise
*/
export declare const fetchUserProfileCompletedness: (user: AuthUser, force?: boolean) => Promise;
+export declare const confirmProfileData: (userHandle: string) => Promise;
diff --git a/types/src/lib/nudge-app/banner-manager.d.ts b/types/src/lib/nudge-app/banner-manager.d.ts
new file mode 100644
index 00000000..98a9ac99
--- /dev/null
+++ b/types/src/lib/nudge-app/banner-manager.d.ts
@@ -0,0 +1,7 @@
+import type { ProfileCompletionData } from 'lib/app-context/profile-completion.model';
+/**
+ * @param completednessData
+ * @returns
+ */
+export declare const getBanner: (completednessData: ProfileCompletionData) => any;
+export declare const hide: () => void;