Skip to content

Commit a2884d3

Browse files
authored
feat: add skeleton loading (#15)
1 parent 29f3264 commit a2884d3

File tree

29 files changed

+1179
-148
lines changed

29 files changed

+1179
-148
lines changed

packages/discord-user-card/src/functions/Renderer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import type { DiscordUserCardProperties } from "@discord-user-card/core";
33
export abstract class Renderer<Props = Required<DiscordUserCardProperties>> {
44
abstract readonly parent: Element;
55
abstract render(props: Props): Promise<void>;
6+
abstract renderSkeleton(props: Props): void;
67
abstract destroy(): void;
78
}
89

910
export interface RendererType {
1011
render: (props: DiscordUserCardProperties) => Promise<void>;
12+
renderSkeleton: (props: DiscordUserCardProperties) => void;
1113
destroy: () => void;
1214
}

packages/discord-user-card/src/renderers/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ export function setupDiscordUserCard(
5353
user,
5454
});
5555
},
56+
renderSkeleton: (props) => {
57+
const { activities, user } = {
58+
...defaultUserCardProperties,
59+
...props,
60+
};
61+
renderer.renderSkeleton({
62+
activities: activities.filter(Boolean),
63+
user,
64+
});
65+
},
5666
destroy: renderer.destroy.bind(renderer),
5767
};
5868
}

packages/discord-user-card/src/renderers/original/card/activities.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { DiscordUserCardProperties } from "@discord-user-card/core";
22
import { ActivityType } from "@discord-user-card/core";
33
import type { Renderer } from "../../../functions/Renderer.js";
44
import { ActivityContentRenderer, ButtonsRenderer, TimebarRenderer, mapActivity } from "../shared/activities.js";
5-
import { addElement, clearUnexpectedAttributes, destoryChildren, removeElement, renderChildren, setClasses } from "../../util.js";
5+
import { addElement, clearUnexpectedAttributes, destoryChildren, removeElement, renderChildren, renderChildrenSkeleton, setClasses } from "../../util.js";
66

77
export class ActivitiesRender implements Renderer {
88
elements = {
@@ -20,7 +20,7 @@ export class ActivitiesRender implements Renderer {
2020

2121
constructor(public readonly parent: Element) { }
2222

23-
async render(props: Required<DiscordUserCardProperties>): Promise<void> {
23+
private _render(props: Required<DiscordUserCardProperties>, skeleton = false) {
2424
// ? Get the activity
2525
const { activities } = props;
2626
const rawActivity = activities.find(activity => activity.type !== ActivityType.Custom);
@@ -63,10 +63,32 @@ export class ActivitiesRender implements Renderer {
6363
// ? Render the elements
6464
addElement(this.parent, this.elements.section);
6565
addElement(this.elements.section, this.elements.headerContainer);
66-
this.elements.header.textContent = activity.title;
66+
if (skeleton) {
67+
const titlePill = document.createElement("span");
68+
setClasses(titlePill, {
69+
duc_skeleton_pill: true,
70+
});
71+
addElement(this.elements.header, titlePill);
72+
}
73+
else {
74+
this.elements.header.textContent = activity.title;
75+
}
6776
addElement(this.elements.headerContainer, this.elements.header);
6877
addElement(this.elements.section, this.elements.content);
69-
await renderChildren(this.children, activity);
78+
79+
return activity;
80+
}
81+
82+
async render(props: Required<DiscordUserCardProperties>): Promise<void> {
83+
const activity = this._render(props);
84+
if (activity)
85+
await renderChildren(this.children, activity);
86+
}
87+
88+
renderSkeleton(props: Required<DiscordUserCardProperties>): void {
89+
const activity = this._render(props, true);
90+
if (activity)
91+
renderChildrenSkeleton(this.children, activity);
7092
}
7193

7294
destroy(): void {

packages/discord-user-card/src/renderers/original/card/index.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
destoryChildren,
1212
removeElement,
1313
renderChildren,
14+
renderChildrenSkeleton,
1415
setClasses,
1516
setStyles,
1617
} from "../../util.js";
@@ -146,6 +147,77 @@ export class OriginalDiscordUserCard implements Renderer {
146147
await renderChildren(this.children, props);
147148
}
148149

150+
renderSkeleton(props: Required<DiscordUserCardProperties>): void {
151+
// ? Destructure the user and activities from the props, and set them to the default values if they are not provided
152+
const { user } = props;
153+
154+
// ? Clear unexpected attributes from the elements
155+
clearUnexpectedAttributes(this.parent, ["aria-label", "class"]);
156+
clearUnexpectedAttributes(this.containers.outer, ["class", "style"]);
157+
clearUnexpectedAttributes(this.containers.inner, ["class"]);
158+
159+
// ? Set the aria-label and class of the parent element
160+
this.parent.setAttribute("aria-label", user.username);
161+
setClasses(this.parent, { duc_root: true, duc_user_card: true });
162+
163+
// ? Generate the style for the user card
164+
const {
165+
themeOverwrite,
166+
} = getUserTheming(user);
167+
const {
168+
backgroundColor,
169+
buttonColor,
170+
dividerColor,
171+
overlayColor,
172+
primaryColor,
173+
secondaryColor,
174+
roleBackgroundColor,
175+
roleBorderColor,
176+
} = getUserTheming({
177+
id: "0",
178+
username: "username",
179+
});
180+
181+
const stylesOuterContainer: StyleObject = {
182+
"--profile-gradient-primary-color": primaryColor,
183+
"--profile-gradient-secondary-color": secondaryColor,
184+
"--profile-gradient-overlay-color": overlayColor,
185+
"--profile-gradient-button-color": buttonColor,
186+
"--profile-body-background-color": backgroundColor,
187+
"--profile-body-divider-color": dividerColor,
188+
"--profile-role-pill-background-color": roleBackgroundColor,
189+
"--profile-role-pill-border-color": roleBorderColor,
190+
"--custom-theme-mix-base-hsl":
191+
"198.46153846153845 100% 5.098039215686274%",
192+
"--custom-theme-mix-base": "rgb(0,18,26)",
193+
"--custom-theme-mix-text": "rgb(223,240,214)",
194+
"--custom-theme-mix-amount-base": "30%",
195+
"--custom-theme-mix-amount-text": "70%",
196+
};
197+
198+
// ? Set the styles of the element
199+
setStyles(this.containers.outer, stylesOuterContainer);
200+
201+
// ? Generate the classes for the user card
202+
const classesOuterContainer: ClassObject = {
203+
duc_user_card_outer: true,
204+
duc_user_card_themed: !!user.themeColors,
205+
[`theme-${themeOverwrite ?? "dark"}`]: true,
206+
duc_theme_skeleton: true,
207+
};
208+
209+
// ? Set the classes of the element
210+
setClasses(this.containers.outer, classesOuterContainer);
211+
setClasses(this.containers.inner, { duc_user_card_inner: true });
212+
213+
// ? Render the user card
214+
addElement(this.parent, this.masks);
215+
addElement(this.parent, this.containers.outer);
216+
addElement(this.containers.outer, this.containers.inner);
217+
218+
renderChildrenSkeleton(this.children, props);
219+
}
220+
149221
destroy(): void {
150222
removeElement(this.parent, this.containers.outer);
151223
destoryChildren(this.children);

packages/discord-user-card/src/renderers/original/card/infoSection.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class InfoSectionRenderer implements Renderer {
2828

2929
constructor(public readonly parent: Element) { }
3030

31-
async render(props: Required<DiscordUserCardProperties>): Promise<void> {
31+
private setAtrributes() {
3232
// ? Clear unexpected attributes from the elements
3333
clearUnexpectedAttributes(this.elements.section, ["class"]);
3434
clearUnexpectedAttributes(this.elements.divider, ["class"]);
@@ -44,6 +44,10 @@ export class InfoSectionRenderer implements Renderer {
4444
setClasses(this.elements.scroller, {
4545
duc_scroller: true,
4646
});
47+
}
48+
49+
async render(props: Required<DiscordUserCardProperties>): Promise<void> {
50+
this.setAtrributes();
4751

4852
// ? Render the elements
4953
addElement(this.parent, this.elements.section);
@@ -57,6 +61,21 @@ export class InfoSectionRenderer implements Renderer {
5761
await this.children.roles.render(props);
5862
}
5963

64+
renderSkeleton(props: Required<DiscordUserCardProperties>): void {
65+
this.setAtrributes();
66+
67+
// ? Render the elements
68+
addElement(this.parent, this.elements.section);
69+
this.children.username.renderSkeleton(props);
70+
this.children.customStatus.renderSkeleton(props);
71+
addElement(this.elements.section, this.elements.divider);
72+
addElement(this.elements.section, this.elements.scroller);
73+
this.children.aboutMe.renderSkeleton(props);
74+
this.children.memberSince.renderSkeleton();
75+
this.children.activities.renderSkeleton(props);
76+
this.children.roles.renderSkeleton(props);
77+
}
78+
6079
destroy(): void {
6180
removeElement(this.parent, this.elements.section);
6281
destoryChildren(this.elements);

packages/discord-user-card/src/renderers/original/card/roles.ts

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,7 @@ export class RolesRenderer implements Renderer {
1616

1717
constructor(public readonly parent: Element) { }
1818

19-
async render(props: Required<DiscordUserCardProperties>): Promise<void> {
20-
const { user: { roles } } = props;
21-
if (!roles?.length) {
22-
return removeElement(this.parent, this.elements.section);
23-
}
24-
19+
private setAttributes(skeleton = false): void {
2520
// ? Clear unexpected attributes from the elements
2621
clearUnexpectedAttributes(this.elements.section, ["class"]);
2722
clearUnexpectedAttributes(this.elements.title, ["class"]);
@@ -37,13 +32,30 @@ export class RolesRenderer implements Renderer {
3732
setClasses(this.elements.roles, {
3833
duc_roles: true,
3934
});
40-
41-
// ? Set the text of the title element
42-
this.elements.title.textContent = "Roles";
35+
if (skeleton) {
36+
const titlePill = document.createElement("span");
37+
setClasses(titlePill, {
38+
duc_skeleton_pill: true,
39+
});
40+
addElement(this.elements.title, titlePill);
41+
}
4342

4443
// ? Set some attributes of the roles element
4544
this.elements.roles.setAttribute("role", "list");
4645
this.elements.roles.setAttribute("tabindex", "0");
46+
}
47+
48+
async render(props: Required<DiscordUserCardProperties>): Promise<void> {
49+
const { user: { roles } } = props;
50+
if (!roles?.length) {
51+
return removeElement(this.parent, this.elements.section);
52+
}
53+
54+
// ? Set the attributes of the elements
55+
this.setAttributes();
56+
57+
// ? Set the text of the title element
58+
this.elements.title.textContent = "Roles";
4759

4860
// ? Render the elements
4961
addElement(this.parent, this.elements.section);
@@ -69,6 +81,39 @@ export class RolesRenderer implements Renderer {
6981
}
7082
}
7183

84+
renderSkeleton(props: Required<DiscordUserCardProperties>): void {
85+
const { user: { roles } } = props;
86+
if (!roles?.length) {
87+
return removeElement(this.parent, this.elements.section);
88+
}
89+
90+
// ? Set the attributes of the elements
91+
this.setAttributes(true);
92+
93+
// ? Render the elements
94+
addElement(this.parent, this.elements.section);
95+
addElement(this.elements.section, this.elements.title);
96+
addElement(this.elements.section, this.elements.roles);
97+
98+
// ? Sort the roles by position (highest to lowest)
99+
const orderedRoles = roles.sort((a, b) => b.position - a.position);
100+
101+
// ? Render the roles
102+
for (const [index, role] of orderedRoles.entries()) {
103+
if (!this.children[index]) {
104+
this.children[index] = new RoleRenderer(this.elements.roles);
105+
}
106+
this.children[index].renderSkeleton(role);
107+
}
108+
109+
// ? Remove any extra roles
110+
for (let i = orderedRoles.length; i < Object.keys(this.children).length; i++) {
111+
const index = i.toString();
112+
this.children[index]?.destroy();
113+
delete this.children[index];
114+
}
115+
}
116+
72117
destroy(): void {
73118
removeElement(this.parent, this.elements.section);
74119
destoryChildren(this.children);
@@ -86,7 +131,7 @@ class RoleRenderer implements Renderer<DiscordUserCardRole> {
86131

87132
constructor(public readonly parent: Element) { }
88133

89-
async render(props: DiscordUserCardRole): Promise<void> {
134+
private _render(props: DiscordUserCardRole): void {
90135
const { id, name, color, icon, emoji } = props;
91136

92137
// ? Clear unexpected attributes from the elements
@@ -156,6 +201,14 @@ class RoleRenderer implements Renderer<DiscordUserCardRole> {
156201
addElement(this.elements.nameContainer, this.elements.name);
157202
}
158203

204+
async render(props: DiscordUserCardRole): Promise<void> {
205+
this._render(props);
206+
}
207+
208+
renderSkeleton(props: DiscordUserCardRole): void {
209+
this._render(props);
210+
}
211+
159212
destroy(): void {
160213
removeElement(this.parent, this.elements.role);
161214
}

packages/discord-user-card/src/renderers/original/profile/activities.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ActivityType } from "@discord-user-card/core";
33
import type { Renderer } from "../../../functions/Renderer.js";
44
import type { Activity } from "../shared/activities.js";
55
import { ActivityContentRenderer, ButtonsRenderer, TimebarRenderer, mapActivity } from "../shared/activities.js";
6-
import { addElement, clearUnexpectedAttributes, destoryChildren, removeElement, renderChildren, setClasses } from "../../util.js";
6+
import { addElement, clearUnexpectedAttributes, destoryChildren, removeElement, renderChildren, renderChildrenSkeleton, setClasses } from "../../util.js";
77

88
export class ActivitiesRender implements Renderer {
99
children: {
@@ -41,6 +41,10 @@ export class ActivitiesRender implements Renderer {
4141
}
4242
}
4343

44+
renderSkeleton(): void {
45+
renderChildrenSkeleton(this.children, undefined);
46+
}
47+
4448
destroy(): void {
4549
destoryChildren(this.children);
4650
}
@@ -102,6 +106,11 @@ class ActivityRender implements Renderer<Activity> {
102106
await renderChildren(this.children, props);
103107
}
104108

109+
renderSkeleton(): void {
110+
// ? In Profile card the activites are not rendered in skeleton state (it will always be on the "User Info" tab)
111+
removeElement(this.parent, this.elements.section);
112+
}
113+
105114
destroy(): void {
106115
removeElement(this.parent, this.elements.section);
107116
destoryChildren(this.children);

0 commit comments

Comments
 (0)