Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Locking demo #111

Merged
merged 22 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 390 additions & 38 deletions demo/package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
"deploy:production": "npm run build && netlify deploy --prod"
},
"dependencies": {
"@ably-labs/spaces": "^0.0.12-alpha",
"ably": "^1.2.41",
"@ably-labs/react-hooks": "file:../../react-hooks",
"@ably-labs/spaces": "file:../",
"ably": "^1.2.43",
"classnames": "^2.3.2",
"dayjs": "^1.11.9",
"lodash.assign": "^4.2.0",
Expand All @@ -22,7 +23,9 @@
"nanoid": "^4.0.2",
"random-words": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-contenteditable": "^3.3.7",
"react-dom": "^18.2.0",
"sanitize-html": "^2.11.0"
},
"devDependencies": {
"@types/lodash.assign": "^4.2.7",
Expand All @@ -31,6 +34,7 @@
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-helmet": "^6.1.6",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
Expand Down
5 changes: 4 additions & 1 deletion demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useContext, useEffect } from 'react';
import { Header, SlideMenu, SpacesContext, CurrentSlide, AblySvg, slides } from './components';
import { getRandomName, getRandomColor } from './utils';
import { useMembers } from './hooks';
import { PreviewProvider } from './components/PreviewContext.tsx';

const App = () => {
const space = useContext(SpacesContext);
Expand Down Expand Up @@ -32,7 +33,9 @@ const App = () => {
id="feature-display"
className="absolute gap-12 bg-[#F7F6F9] w-full h-[calc(100%-80px)] -z-10 overflow-y-hidden overflow-x-hidden flex justify-between min-w-[375px] xs:flex-col md:flex-row"
>
<SlideMenu slides={slides} />
<PreviewProvider preview>
<SlideMenu slides={slides} />
</PreviewProvider>
<CurrentSlide slides={slides} />
</section>
</main>
Expand Down
2 changes: 1 addition & 1 deletion demo/src/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cn from 'classnames';
import { type SpaceMember } from '../../../src/types';
import { type SpaceMember } from '@ably-labs/spaces';

import { AvatarInfo } from './AvatarInfo';
import { LightningSvg } from './svg';
Expand Down
2 changes: 1 addition & 1 deletion demo/src/components/AvatarInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cn from 'classnames';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';

import { type SpaceMember } from '../../../src/types';
import { type SpaceMember } from '@ably-labs/spaces';
import { type ProfileData } from '../utils/types';

type Props = Omit<SpaceMember, 'profileData'> & {
Expand Down
65 changes: 65 additions & 0 deletions demo/src/components/EditableText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useCallback, useEffect, useRef } from 'react';
import ContentEditable, { ContentEditableEvent } from 'react-contenteditable';
import sanitize from 'sanitize-html';

interface EditableTextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'children'> {
as?: string;
disabled: boolean;
value: string;
onChange(nextValue: string): void;
maxlength?: number;
className?: string;
}

export const EditableText: React.FC<EditableTextProps> = ({
as,
disabled,
maxlength = 300,
value,
onChange,
...restProps
}) => {
const elementRef = useRef<HTMLElement | null>(null);
const handleTextChange = useCallback(
(evt: ContentEditableEvent) => {
const nextValue = sanitize(evt.target.value, {
allowedTags: [],
});

if (nextValue.length > maxlength) {
onChange(value);
} else {
onChange(nextValue);
}
},
[onChange, value, maxlength],
);

useEffect(() => {
const element = elementRef.current;
if (!disabled && element) {
moveCursorToEnd(element);
}
}, [disabled]);

return (
<ContentEditable
tagName={as}
innerRef={elementRef}
disabled={disabled}
html={value}
onChange={handleTextChange}
{...restProps}
/>
);
};

const moveCursorToEnd = (el: HTMLElement) => {
el.focus();
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
};
20 changes: 11 additions & 9 deletions demo/src/components/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,31 @@ interface Props extends React.HTMLAttributes<HTMLImageElement> {
className?: string;
id: string;
slide: string;
locatable?: boolean;
}

export const Image = ({ src, children, className, id, slide }: Props) => {
const { members } = useMembers();
const { handleSelect } = useElementSelect(id);
export const Image = ({ src, children, className, id, slide, locatable = true }: Props) => {
const { members, self } = useMembers();
const { handleSelect } = useElementSelect(id, false);
const activeMember = findActiveMember(id, slide, members);
const name = getMemberFirstName(activeMember);
const outlineClasses = getOutlineClasses(activeMember);
const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember);
const memberName = getMemberFirstName(activeMember);
const label = self?.connectionId === activeMember?.connectionId ? 'You' : memberName;

return (
<div
data-before={name}
data-before={label}
className={cn('relative xs:my-4 md:my-0', className, {
[`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses}`]:
!!activeMember,
[`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses} before:${stickyLabelClasses}`]:
activeMember,
})}
>
<img
id={id}
data-id="slide-image-placeholder"
className="cursor-pointer block"
src={src}
onClick={handleSelect}
onClick={locatable ? handleSelect : undefined}
/>
{children ? children : null}
</div>
Expand Down
83 changes: 60 additions & 23 deletions demo/src/components/Paragraph.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,73 @@
import React, { useRef } from 'react';
import cn from 'classnames';
import { useElementSelect, useMembers } from '../hooks';
import { findActiveMember, getMemberFirstName, getOutlineClasses } from '../utils';
import { getMemberFirstName, getOutlineClasses } from '../utils';
import { StickyLabel } from './StickyLabel';
import { LockFilledSvg } from './svg/LockedFilled.tsx';
import { EditableText } from './EditableText.tsx';
import { useTextComponentLock } from '../hooks/useTextComponentLock.ts';

interface Props extends React.HTMLAttributes<HTMLParagraphElement> {
id: string;
slide: string;
variant?: 'regular' | 'aside';
children: string;
maxlength?: number;
}

export const Paragraph = ({ variant = 'regular', id, slide, className, ...props }: Props) => {
const { members } = useMembers();
const { handleSelect } = useElementSelect(id);
const activeMember = findActiveMember(id, slide, members);
const name = getMemberFirstName(activeMember);
const outlineClasses = getOutlineClasses(activeMember);
export const Paragraph = ({
variant = 'regular',
id,
slide,
className,
children,
maxlength = 300,
...props
}: Props) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const { content, activeMember, locked, lockedByYou, editIsNotAllowed, handleSelect, handleContentUpdate } =
useTextComponentLock({
id,
slide,
defaultText: children,
containerRef,
});
const memberName = getMemberFirstName(activeMember);
const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember);

return (
<p
id={id}
data-before={name}
className={cn(
'text-ably-avatar-stack-demo-slide-text cursor-pointer relative',
{
'xs:w-auto text-xs xs:text-base md:text-lg xs:my-4 md:my-0': variant === 'regular',
'text-[13px] p-0 leading-6': variant === 'aside',
[`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses}`]:
!!activeMember,
},
className,
)}
<div
ref={containerRef}
{...props}
onClick={handleSelect}
/>
className="relative"
onClick={locked ? undefined : handleSelect}
>
<StickyLabel
visible={locked}
className={`${stickyLabelClasses} flex flex-row items-center`}
>
{lockedByYou ? 'You' : memberName}
{editIsNotAllowed && <LockFilledSvg className="text-white" />}
</StickyLabel>
<EditableText
as="p"
id={id}
disabled={!lockedByYou}
value={content}
onChange={handleContentUpdate}
maxlength={maxlength}
className={cn(
'text-ably-avatar-stack-demo-slide-text break-all',
{
'xs:w-auto text-xs xs:text-base md:text-lg xs:my-4 md:my-0': variant === 'regular',
'text-[13px] p-0 leading-6': variant === 'aside',
[`outline-2 outline ${outlineClasses}`]: locked,
'cursor-pointer': !locked,
'cursor-not-allowed': editIsNotAllowed,
'bg-slate-200': editIsNotAllowed,
},
className,
)}
/>
</div>
);
};
13 changes: 13 additions & 0 deletions demo/src/components/PreviewContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { useContext } from 'react';

interface PreviewContextProviderProps {
preview: boolean;
children: React.ReactNode;
}
const PreviewContext = React.createContext<boolean>(false);

export const PreviewProvider: React.FC<PreviewContextProviderProps> = ({ preview, children }) => (
<PreviewContext.Provider value={preview}>{children}</PreviewContext.Provider>
);

export const usePreview = () => useContext<boolean>(PreviewContext);
4 changes: 2 additions & 2 deletions demo/src/components/SlidePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const SlidePreview = ({ children, index }: SlidePreviewProps) => {
const membersOnASlide = (members || []).filter(({ location }) => location?.slide === `${index}`);
const isActive = self?.location?.slide === `${index}`;

const handleSlideClick = () => {
const handleSlideClick = async () => {
if (!space || !self) return;
space.locations.set({ slide: `${index}`, element: null });
};
Expand Down Expand Up @@ -43,7 +43,7 @@ export const SlidePreview = ({ children, index }: SlidePreviewProps) => {
</p>
<div
data-id="slide-preview-container"
className="relative rounded-[30px] border-2 border-ably-avatar-stack-demo-slide-preview-border w-[1020px] h-[687px] min-w-[1020px] min-h-[687px] bg-white"
className="relative rounded-[30px] border-2 border-ably-avatar-stack-demo-slide-preview-border w-[1020px] h-[687px] min-w-[1020px] min-h-[687px] bg-white pointer-events-none"
>
{children}
</div>
Expand Down
24 changes: 24 additions & 0 deletions demo/src/components/SlidesStateContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { useMemo, useState } from 'react';

interface SlidesStateContextProps {
slidesState: Record<string, string>;
setContent(id: string, nextContent: string): void;
}
export const SlidesStateContext = React.createContext<SlidesStateContextProps>({
slidesState: {},
setContent: () => {},
});

export const SlidesStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [slidesState, setSlidesState] = useState<Record<string, string>>({});
const value = useMemo(
() => ({
slidesState,
setContent: (id: string, nextContent: string) => {
setSlidesState((prevState) => ({ ...prevState, [id]: nextContent }));
},
}),
[slidesState, setSlidesState],
);
return <SlidesStateContext.Provider value={value}>{children}</SlidesStateContext.Provider>;
};
4 changes: 2 additions & 2 deletions demo/src/components/SpacesContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as React from 'react';
import Spaces, { type Space } from '../../../src/index';
import Spaces, { type Space } from '@ably-labs/spaces';
import { Realtime } from 'ably';
import { nanoid } from 'nanoid';

import { getSpaceNameFromUrl } from '../utils';

const clientId = nanoid();

const ably = new Realtime.Promise({
export const ably = new Realtime.Promise({
authUrl: `/api/ably-token-request?clientId=${clientId}`,
clientId,
});
Expand Down
18 changes: 18 additions & 0 deletions demo/src/components/StickyLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

interface StickyLabelProps {
visible: boolean;
className?: string;
children: React.ReactNode;
}
export const StickyLabel: React.FC<StickyLabelProps> = ({ visible, className, children }) => {
if (!visible) return null;

return (
<div
className={`absolute -top-[22px] -left-[2px] px-[10px] text-sm text-white rounded-t-lg normal-case ${className}`}
>
{children}
</div>
);
};
Loading