-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ca852cf
commit 8c619a5
Showing
23 changed files
with
4,394 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
VITE_PUBLIC_ABLY_KEY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.