Skip to content

Conversation

@vas3a
Copy link
Collaborator

@vas3a vas3a commented Jan 19, 2026

Added banners to act as periodic reminders for members to keep their profile up to date.

  • banner will show up after 90 days after your last update or confirmation (confirmation stored in DB)
  • if you dismiss banner it will show up again after 3 days (flag stored via cookie)
  • The banner also shows the section you haven't updated in the longest time (oldest one without an udpate)

UI:

  • clicking the banner text, or "update" will redirect user to profile page
  • clicking confirm will mark profile as confirmed, but there is a "confirmation modal"
  • clicking "x" button will dismiss banner for 3 days
  • on profile page, the banner changes UI: it only shows text & CTA to confirm profile + dismiss button
image image image image

@vas3a vas3a requested a review from kkartunov January 19, 2026 06:12
} = $ctx.auth)

let toast: ToastType | undefined;
let banner: {key: string, date: Date} | undefined;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Consider defining a type for the banner object to improve type safety and maintainability. This will make it easier to understand the structure of the banner object and prevent potential errors.

<Banner
userHandle={user?.handle ?? ''}
banner={banner}
redirect={toastsMeta[banner.key].ctaLink(user?.handle ?? '')}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Ensure that toastsMeta[banner.key] is always defined before accessing ctaLink. If banner.key is not present in toastsMeta, this could lead to a runtime error.

return getCookieValue(COOKIE_NAME);
}

const isOlderThanTreshold = (date: Date | number, treshold: number): boolean => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The function name isOlderThanTreshold contains a typo in Treshold. It should be Threshold for clarity and consistency.

return;
}

const lastUpdateOrCofirmDate = lastProfileConfirmationDate ? Math.max(lastUpdate, +lastProfileConfirmationDate) : lastUpdate;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The variable name lastUpdateOrCofirmDate contains a typo in Cofirm. It should be Confirm for clarity and consistency.

.sort((a, b) => +a[1] - +b[1]);


const lastUpdate = +sorted[sorted.length - 1][1];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Accessing sorted[sorted.length - 1] without checking if sorted is empty could lead to an error. Consider adding a check to ensure sorted has elements before accessing.

}
}

return JSON.parse(lastSeen);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The function getBanner returns the result of JSON.parse(lastSeen) without checking if lastSeen is a valid JSON string. Consider adding error handling for JSON.parse to prevent runtime errors.

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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The visibility transition duration is set to 0.01ms, which is effectively instantaneous and may not have the intended effect. Consider removing it or setting it to a more meaningful duration.

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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The visibility transition duration is set to 0.01ms, which is effectively instantaneous and may not have the intended effect. Consider removing it or setting it to a more meaningful duration.

border-radius: 0.25rem;

transition: background 0.15s ease;
cursor: pointer;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The cursor: pointer; property is declared twice in the .inlineBtn class. Consider removing the duplicate declaration to improve maintainability.


function dismiss() {
visible = false;
setTimeout(dispatch, 300, 'dismiss');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using setTimeout with dispatch may lead to unexpected behavior if the component is unmounted before the timeout completes. Consider using a more reliable method to handle the dispatch, such as checking if the component is still mounted.

}

async function confirm() {
await confirmProfileData(userHandle)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The confirmProfileData function is awaited, but there is no error handling. Consider adding a try-catch block to handle potential errors during the confirmation process.

return `${day}${suffix} ${months[d.getMonth()]} ${d.getFullYear()}`;
}

onMount(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
The setTimeout function is used to set visible to true after 15 milliseconds. This might be unnecessary and could be replaced with a more direct approach unless there is a specific reason for the delay.

</div>
{:else}
<div class={styles.contents}>
<a href={redirectUrl} target="_blank">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
The anchor tag uses target="_blank" without rel="noopener noreferrer". This can be a security risk as it allows the new page to access the window object of the page that opened it. Consider adding rel="noopener noreferrer" to mitigate this risk.


<div class={styles.spacer} />
<a
href={redirectUrl}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
The anchor tag uses target="_blank" without rel="noopener noreferrer". This can be a security risk as it allows the new page to access the window object of the page that opened it. Consider adding rel="noopener noreferrer" to mitigate this risk.

background: #D4D4D4;
width: 1px;
height: 60px;
height: var(--uninav-header-height);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Using a CSS variable var(--uninav-header-height) for the height is a good practice for maintainability and consistency. Ensure that this variable is defined and has a fallback value to prevent potential rendering issues if the variable is not set.

instancesContextStore[targetId] = ctx;

if (navType === 'tool' || navType === 'marketing') {
await loadNudgeApp(ctx, targetEl as Element);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
The await keyword is used with loadNudgeApp, but it was not used in the previous implementation. Ensure that loadNudgeApp is intended to be asynchronous and that this change does not introduce unintended side effects, such as delaying the loading of the navigation component.

handle: string;
percentComplete: number;
showToast: string;
dateFields?: [string, Date][];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
Consider using a more descriptive type for dateFields instead of [string, Date][]. This will improve readability and maintainability by making it clear what each element of the tuple represents. For example, you could define a type alias like type DateField = [fieldName: string, lastUpdated: Date]; and use DateField[].

data: {
percentComplete: number;
} & {
[key: string]: any;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Using [key: string]: any in the ProfileCompletednessResponse interface can lead to potential type safety issues. Consider defining specific keys or using a more precise type instead of any to improve type safety and maintainability.

Copy link
Contributor

@kkartunov kkartunov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@vas3a vas3a merged commit 8bfea14 into dev Jan 19, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants