Skip to content

Commit

Permalink
Add Spaces Avatar Stack example
Browse files Browse the repository at this point in the history
  • Loading branch information
GregHolmes committed Dec 16, 2024
1 parent ca852cf commit 8c619a5
Show file tree
Hide file tree
Showing 23 changed files with 4,394 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/spaces-avatar-stack/javascript/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_PUBLIC_ABLY_KEY=
36 changes: 36 additions & 0 deletions examples/spaces-avatar-stack/javascript/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
39 changes: 39 additions & 0 deletions examples/spaces-avatar-stack/javascript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Display active users avatars in an application

This folder contains the code for the avatar stack (Typescript) - a demo of how you can leverage [Ably Spaces](https://github.com/ably/spaces) to show a list of currently online users.

## Getting started

1. Clone the [Ably docs](https://github.com/ably/docs) repository where this example can be found:

```sh
git clone [email protected]:ably/docs.git
```

2. Change directory:

```sh
cd /examples/spaces-avatar-stack/javascript/
```

3. Rename the environment file:

```sh
mv .env.example .env.local
```

4. In `.env.local` update the value of `VITE_PUBLIC_ABLY_KEY` to be your Ably API key.

5. Install dependencies:

```sh
yarn install
```

6. Run the server:

```sh
yarn run dev
```

7. Try it out by opening two tabs to [http://localhost:5173/](http://localhost:5173/) with your browser to see the result.
17 changes: 17 additions & 0 deletions examples/spaces-avatar-stack/javascript/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Avatar stack</title>
<link rel="stylesheet" href="src/styles.css" />
</head>
<body>
<div id="app">
<div id="avatar-stack" class="avatarStackContainer">
<div id="avatars" class="avatars"></div>
</div>
</div>
<script type="module" src="src/script.ts"></script>
</body>
</html>
22 changes: 22 additions & 0 deletions examples/spaces-avatar-stack/javascript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "js-avatar-stack",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"dotenv": "^16.4.5",
"typescript": "^5.5.4",
"@faker-js/faker": "^9.2.0"
},
"dependencies": {
"@ably/spaces": "^0.4.0",
"ably": "^2.3.1",
"nanoid": "^5.0.7",
"vite": "^5.4.2"
}
}
162 changes: 162 additions & 0 deletions examples/spaces-avatar-stack/javascript/src/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import Spaces, { type SpaceMember } from '@ably/spaces';
import { Realtime } from 'ably';
import { nanoid } from 'nanoid';
import { faker } from '@faker-js/faker';

export type Member = Omit<SpaceMember, 'profileData'> & {
profileData: { memberColor: string; name: string };
};

connect();

async function connect() {
const client = new Realtime({
clientId: nanoid(),
key: import.meta.env.VITE_PUBLIC_ABLY_KEY as string,
});

const spaces = new Spaces(client);
const space = await spaces.get('avatar-stack');

/** 💡 Add every avatar that enters 💡 */
space.members.subscribe(['leave', 'remove'], (memberUpdate: SpaceMember) => {
const avatar = document.querySelector(`[data-member-id="${memberUpdate.clientId}"]`);
if (avatar) {
avatar.remove();
}
});
space.members.subscribe('enter', (memberUpdate: SpaceMember) => {
const member: Member = {
...memberUpdate,
profileData: {
memberColor: (memberUpdate.profileData as any).memberColor,
name: (memberUpdate.profileData as any).name,
},
};
renderAvatar(member, true);
});

/** 💡 Enter the space as soon as it's available 💡 */
space
.enter({
name: faker.person.firstName() + ' ' + faker.person.lastName(),
memberColor: faker.color.rgb({ format: 'hex', casing: 'lower' }),
})
.then(async () => {
const otherMembers = await space.members.getOthers();

/** 💡 Get first four except the local member in the space 💡 */
otherMembers.slice(0, 4).forEach((member) => {
renderAvatar(member as Member);
});

/** 💡 Get a count of the number exceeding four and display as a single tally 💡 */
renderExceedingCounter(otherMembers);
})
.catch((err) => {
console.error('Error joining space:', err);
});
}

function buildUserInfo(member: Member, isSelf: boolean = false): HTMLDivElement {
const wrapper = document.createElement('div');
wrapper.className = 'wrapper';

const userInfoContainer = document.createElement('div');
userInfoContainer.className = 'userInfoContainer';
userInfoContainer.style.backgroundColor = member.profileData.memberColor;
userInfoContainer.id = 'avatar';

const userInitials = member.profileData.name
.split(' ')
.map((word: string) => word.charAt(0))
.join('');

const initials = document.createElement('p');
initials.className = 'smallText';
initials.textContent = userInitials;

userInfoContainer.appendChild(initials);
wrapper.appendChild(userInfoContainer);

const userList = document.createElement('div');
userList.className = 'userList';
userList.id = 'user-list';

const nameElement = document.createElement('p');
nameElement.className = 'name';
nameElement.textContent = isSelf ? member.profileData.name + ' (You)' : member.profileData.name;

userList.appendChild(nameElement);
wrapper.appendChild(userList);

return wrapper;
}

async function renderAvatar(member: Member, isSelf: boolean = false): Promise<void> {
console.log(member);
const userInitials = member.profileData.name
.split(' ')
.map((word: string) => word.charAt(0))
.join('');

const avatarsElement = document.getElementById('avatars');
if (avatarsElement) {
const avatarElement = document.createElement('div');
avatarElement.className = isSelf ? 'selfAvatar' : 'otherAvatar';

const avatarContainer = document.createElement('div');
avatarContainer.className = 'avatarContainer';

const avatar = document.createElement('div');
avatar.className = 'avatar';
avatar.style.backgroundColor = member.profileData.memberColor;
avatar.setAttribute('data-member-id', member['clientId']);
avatar.setAttribute('key', member['clientId']);

const initials = document.createElement('p');
initials.className = 'textWhite';
initials.textContent = userInitials;

avatar.appendChild(initials);

const popup = document.createElement('div');
popup.className = 'popup';
popup.style.display = 'none';

const userInfo = buildUserInfo(member, isSelf);
avatarElement.appendChild(avatarContainer);
avatarContainer.appendChild(avatar);
popup.appendChild(userInfo);
avatar.appendChild(popup);

avatarsElement.appendChild(avatarElement);

avatar.addEventListener('mouseover', () => {
popup.style.display = 'block';
});

avatar.addEventListener('mouseleave', () => {
popup.style.display = 'none';
});
}
}

function renderExceedingCounter(otherMembers: SpaceMember[]) {
if (otherMembers.length > 4) {
const avatarsElement = document.getElementById('avatars');

if (avatarsElement) {
const avatarElement = document.createElement('div');
avatarElement.className = 'avatar';
avatarElement.style.backgroundColor = '#595959';

const nameElement = document.createElement('p');
nameElement.className = 'textWhite nameOthers';
nameElement.textContent = `+${otherMembers.length - 4}`;

avatarElement.appendChild(nameElement);
avatarsElement.appendChild(avatarElement);
}
}
}
84 changes: 84 additions & 0 deletions examples/spaces-avatar-stack/javascript/src/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
.avatarStackContainer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
background-color: #f4f8fb;
height: 100vh;
}

.avatars {
display: inline-flex;
}

.avatarContainer {
position: relative;
}

.avatar {
background-color: rgb(234, 88, 12);
height: 3rem;
width: 3rem;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #e2e7ef;
flex-shrink: 0;
position: relative;
margin-left: -7px;
}

.name {
font-size: 0.75rem;
line-height: 1rem;
color: rgb(255, 255, 255);
}

.textWhite {
color: #fff;
}

.nameOthers {
z-index: 20;
font-size: 0.75rem;
}

.popup {
position: absolute;
left: -5rem;
top: -4.8rem;
padding: 1rem;
background-color: #000;
border-radius: 8px;
color: #fff;
min-width: 240px;
}

.wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
}

.userList {
padding-left: 0.75rem;
width: 100%;
}

.userInfoContainer {
height: 2rem;
width: 2rem;
border-radius: 9999px;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
position: relative;
border: 2px solid #cbd5e0;
}

.smallText {
font-size: 0.75rem;
}
Loading

0 comments on commit 8c619a5

Please sign in to comment.