Skip to content

Commit

Permalink
feat(astro): Add support for custom pages and links (#3987)
Browse files Browse the repository at this point in the history
  • Loading branch information
wobsoriano authored Aug 22, 2024
1 parent 540b720 commit d426de6
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/big-trains-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/astro": minor
---

Add support for custom pages and links in the `<UserProfile />` Astro component.
22 changes: 22 additions & 0 deletions integration/templates/astro-node/src/pages/custom-pages.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
import { UserProfile } from "@clerk/astro/components";
import Layout from "../layouts/Layout.astro";
---

<Layout title="Custom Pages">
<div class="w-full flex justify-center">
<UserProfile path="/custom-pages">
<UserProfile.Page label="Terms" url="terms">
<div slot="label-icon">Icon</div>
<div>
<h1>Custom Terms Page</h1>
<p>This is the custom terms page</p>
</div>
</UserProfile.Page>
<UserProfile.Link label="Homepage" url="/">
<div slot="label-icon">Icon</div>
</UserProfile.Link>
<UserProfile.Page label="security" />
</UserProfile>
</div>
</Layout>
27 changes: 27 additions & 0 deletions integration/tests/astro/components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,33 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
await expect(u.page.getByText(`"firstName":"${fakeAdmin.firstName}"`)).toBeVisible();
});

test('render user profile with custom pages and links', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/sign-in');
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();

await u.page.goToRelative('/custom-pages');
await u.po.userProfile.waitForMounted();

// Check if custom pages and links are visible
await expect(u.page.getByRole('button', { name: /Terms/i })).toBeVisible();
await expect(u.page.getByRole('button', { name: /Homepage/i })).toBeVisible();

// Navigate to custom page
await u.page.getByRole('button', { name: /Terms/i }).click();
await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible();

// Check reordered default label. Security tab is now the last item.
await u.page.locator('.cl-navbarButton').nth(3).click();
await expect(u.page.getByRole('heading', { name: 'Security' })).toBeVisible();

// Click custom link and check navigation
await u.page.getByRole('button', { name: /Homepage/i }).click();
await u.page.waitForAppUrl('/');
});

test('redirects to sign-in when unauthenticated', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/user');
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ module.exports = {
rules: {
'import/no-unresolved': ['error', { ignore: ['^#'] }],
},
ignorePatterns: ['src/astro-components/index.ts', 'src/astro-components/interactive/UserButton/index.ts'],
ignorePatterns: [
'src/astro-components/index.ts',
'src/astro-components/interactive/UserButton/index.ts',
'src/astro-components/interactive/UserProfile/index.ts',
],
overrides: [
{
files: ['./env.d.ts'],
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/astro-components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export { default as SignOutButton } from './unstyled/SignOutButton.astro';
export { default as SignIn } from './interactive/SignIn.astro';
export { default as SignUp } from './interactive/SignUp.astro';
export { UserButton } from './interactive/UserButton';
export { default as UserProfile } from './interactive/UserProfile.astro';
export { UserProfile } from './interactive/UserProfile';
export { default as OrganizationProfile } from './interactive/OrganizationProfile.astro';
export { default as OrganizationSwitcher } from './interactive/OrganizationSwitcher.astro';
export { default as OrganizationList } from './interactive/OrganizationList.astro';
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
interface Props {
url: string
label: string
type: 'page' | 'link'
}
const { url, label, type } = Astro.props
let labelIcon = '';
let content = ''
if (Astro.slots.has('label-icon')) {
labelIcon = await Astro.slots.render('label-icon');
}
if (Astro.slots.has('default') && type === 'page') {
content = await Astro.slots.render('default');
}
---

<script is:inline define:vars={{ url, label, content, labelIcon, type }}>
// Get the user profile map from window that we set in the `<InternalUIComponentRenderer />`.
const userProfileComponentMap = window.__astro_clerk_component_props.get('user-profile');

const userProfile = document.querySelector('[data-clerk-id^="clerk-user-profile"]');

const safeId = userProfile.getAttribute('data-clerk-id');
const currentOptions = userProfileComponentMap.get(safeId);

const reorderItemsLabels = ['account', 'security'];
const isReorderItem = reorderItemsLabels.includes(label);

let newCustomPage = { label }

if (!isReorderItem) {
newCustomPage = {
...newCustomPage,
url,
mountIcon: (el) => { el.innerHTML = labelIcon },
unmountIcon: () => { /* Implement cleanup if needed */ }
}

if (type === 'page') {
newCustomPage = {
...newCustomPage,
mount: (el) => { el.innerHTML = content },
unmount: () => { /* Implement cleanup if needed */ }
}
}
}

userProfileComponentMap.set(safeId, {
...currentOptions,
customPages: [
...(currentOptions?.customPages ?? []),
newCustomPage,
]
})
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import type { UserProfileProps, Without } from '@clerk/types'
type Props = Without<UserProfileProps, 'customPages'>
import InternalUIComponentRenderer from '../InternalUIComponentRenderer.astro'
---

<InternalUIComponentRenderer {...Astro.props} component="user-profile" />
<slot />
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
import ProfileRenderer from './ProfileRenderer.astro'
interface Props {
url: string
label: string
}
const { url, label } = Astro.props
---

<ProfileRenderer label={label} url={url} type="link">
<slot name="label-icon" slot="label-icon" />
</ProfileRenderer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
import ProfileRenderer from './ProfileRenderer.astro'
type ReorderItemsLabels = 'account' | 'security'
type Props<Label extends string> = {
label: Label
} & (Label extends ReorderItemsLabels
? {
url?: string
}
: {
url: string
}
)
const { url, label } = Astro.props
---

<ProfileRenderer label={label} url={url} type="page">
<slot name="label-icon" slot="label-icon" />
<slot />
</ProfileRenderer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import _UserProfile from './UserProfile.astro';
import UserProfileLink from './UserProfileLink.astro';
import UserProfilePage from './UserProfilePage.astro';

export const UserProfile = Object.assign(_UserProfile, {
Page: UserProfilePage,
Link: UserProfileLink,
});

0 comments on commit d426de6

Please sign in to comment.