adding previous carousel as starting point

This commit is contained in:
Roland Osborne 2022-08-23 00:06:33 -07:00
parent d21bbaf349
commit b5f3ee2710
11 changed files with 574 additions and 14 deletions

View File

@ -0,0 +1,162 @@
import React, { useState, useEffect, useRef } from 'react';
import { Skeleton } from 'antd';
import { CarouselWrapper } from './Carousel.styled';
import { RightOutlined, LeftOutlined, CloseOutlined, PictureOutlined, FireOutlined } from '@ant-design/icons';
import ReactResizeDetector from 'react-resize-detector';
export function Carousel({ ready, error, items, itemRenderer, itemRemove }) {
const [slots, setSlots] = useState([]);
const [carouselRef, setCarouselRef] = useState(false);
const [itemIndex, setItemIndex] = useState(0);
const [scrollLeft, setScrollLeft] = useState('hidden');
const [scrollRight, setScrollRight] = useState('hidden');
const FUDGE = 1;
let carousel = useRef();
let itemWidth = useRef(new Map());
useEffect(() => {
setScroll('smooth');
setArrows();
}, [itemIndex, items]);
useEffect(() => {
setScroll('auto');
}, [carouselRef]);
const updateItemIndex = (val) => {
setItemIndex((i) => {
if (i + val < 0) {
return 0;
}
return i + val;
})
}
const onLeft = () => {
if (itemIndex > 0) {
updateItemIndex(-1);
}
}
const onRight = () => {
if(itemIndex + 1 < items.length) {
updateItemIndex(+1);
}
}
const setScroll = (behavior) => {
let pos = FUDGE;
for (let i = 0; i < itemIndex; i++) {
pos += itemWidth.current.get(i) + 32;
}
if (carousel.current) {
carousel.current.scrollTo({ top: 0, left: pos, behavior });
}
}
const setArrows = () => {
if (itemIndex == 0) {
setScrollLeft('hidden');
}
else {
setScrollLeft('unset');
}
if (itemIndex + 1 >= items.length) {
setScrollRight('hidden');
}
else {
setScrollRight('unset');
}
}
const RemoveItem = ({ index }) => {
if (itemRemove) {
return <div class="delitem" onClick={() => itemRemove(index)}><CloseOutlined /></div>
}
return <></>
}
useEffect(() => {
let assets = [];
if (ready) {
for (let i = 0; i < items.length; i++) {
assets.push((
<ReactResizeDetector handleWidth={true} handleHeight={false}>
{({ width, height }) => {
itemWidth.current.set(i, width);
return (
<div class="item noselect">
<div class="asset">{ itemRenderer(items[i], i) }</div>
<RemoveItem index={i} />
</div>
);
}}
</ReactResizeDetector>
));
}
if (items.length > 0) {
assets.push(<div class="space">&nbsp;</div>)
}
if (itemIndex >= items.length) {
if (items.length > 0) {
setItemIndex(items.length - 1);
}
else {
setItemIndex(0);
}
}
}
setSlots(assets);
setScroll();
setArrows();
}, [ready, items]);
const onRefSet = (r) => {
if (r != null) {
carousel.current = r;
setCarouselRef(true);
}
}
if (!ready || error) {
return (
<CarouselWrapper>
<div class="carousel">
{error && (
<div class="status">
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
</div>
)}
{!ready && !error && (
<div class="status">
<PictureOutlined style={{ fontSize: 32 }} />
</div>
)}
</div>
<div class="arrows">
<div class="arrow" onClick={onRight}><LeftOutlined style={{ visibility: 'hidden' }} /></div>
<div class="arrow" onClick={onLeft}><RightOutlined style={{ visibility: 'hidden' }} /></div>
</div>
</CarouselWrapper>
)
}
if (slots.length != 0) {
return (
<CarouselWrapper>
<div class="carousel" ref={onRefSet}>
{slots}
</div>
<div class="arrows">
<div class="arrow" onClick={onRight}><LeftOutlined style={{ visibility: scrollRight }} /></div>
<div class="arrow" onClick={onLeft}><RightOutlined style={{ visibility: scrollLeft }} /></div>
</div>
</CarouselWrapper>
);
}
return <></>
}

View File

@ -0,0 +1,89 @@
import styled from 'styled-components';
export const CarouselWrapper = styled.div`
position: relative;
display: grid;
width: 100%;
height: 128px;
margin-top: 16px;
.carousel {
display: flex;
flex-direction: row;
padding-left: 16px;
width: 100%;
overflow: hidden;
/* hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none;
scrollbar-width: none;
}
.status {
width: 128px;
height: 128px;
display: flex;
align-items: center;
justify-content: center;
color: #888888;
background-color: #eeeeee;
}
.carousel::-webkit-scrollbar {
display: none;
}
.arrows {
height: 100%;
display: flex;
flex-direction: column;
position: absolute;
}
.arrow {
height: 50%;
background-color: #888888;
color: white;
font-size: 16px;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
}
.arrow:hover {
opacity: 1;
}
.item {
margin-right: 32px;
position: relative;
}
.delitem {
position: absolute;
top: 0;
right: 0;
background-color: #888888;
color: white;
border-bottom-left-radius: 2px;
padding-left: 2px;
padding-right: 2px;
cursor: pointer;
}
.asset {
height: 128px;
}
.space {
height: 128px;
padding-left: 100%;
}
.object {
height: 100%;
object-fit: contain;
}
`;

View File

@ -0,0 +1,21 @@
import { useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
export function useCarousel() {
const [state, setState] = useState({
});
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const navigate = useNavigate();
const actions = {
};
return { state, actions };
}

View File

@ -36,7 +36,7 @@ console.log(state);
<div class="line" />
</div>
<div class="topic">
<AddTopic />
<AddTopic cardId={cardId} channelId={channelId} />
</div>
</ConversationWrapper>
);

View File

@ -4,18 +4,56 @@ import { Input, Menu, Dropdown } from 'antd';
import { useRef, useState } from 'react';
import { FontColorsOutlined, FontSizeOutlined, PaperClipOutlined, SendOutlined } from '@ant-design/icons';
import { SketchPicker } from "react-color";
import { AudioFile } from './audioFile/AudioFile';
import { VideoFile } from './videoFile/VideoFile';
import { Carousel } from 'carousel/Carousel';
export function AddTopic() {
const { state, actions } = useAddTopic();
export function AddTopic({ cardId, channelId }) {
const { state, actions } = useAddTopic(cardId, channelId);
const attachImage = useRef(null);
const attachAudio = useRef(null);
const attachVideo = useRef(null);
const msg = useRef();
const keyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
msg.current.blur();
}
}
const onSelectImage = (e) => {
actions.addImage(e.target.files[0]);
attachImage.current.value = '';
}
const onSelectAudio = (e) => {
actions.addAudio(e.target.files[0]);
attachAudio.current.value = '';
}
const onSelectVideo = (e) => {
actions.addVideo(e.target.files[0]);
attachVideo.current.value = '';
}
const renderItem = (item, index) => {
if (item.image) {
return <img style={{ height: '100%', objectFit: 'contain' }} src={item.url} alt="" />
}
if (item.audio) {
return <AudioFile onLabel={(label) => actions.setLabel(index, label)}/>
}
if (item.video) {
return <VideoFile onPosition={(pos) => actions.setPosition(index, pos)} url={item.url} />
}
return <></>
}
const removeItem = (index) => {
actions.removeAsset(index);
}
const picker = (
<Menu style={{ backgroundColor: 'unset', boxShadow: 'unset' }}>
<SketchPicker disableAlpha={true}
@ -37,20 +75,25 @@ export function AddTopic() {
const attacher = (
<Menu>
<Menu.Item key="0">
<div>Add Image</div>
<input type='file' name="asset" accept="image/*" ref={attachImage} onChange={e => onSelectImage(e)} style={{display: 'none'}}/>
<div onClick={() => attachImage.current.click()}>Attach Image</div>
</Menu.Item>
<Menu.Item key="1">
<div>Add Video</div>
<input type='file' name="asset" accept="audio/*" ref={attachAudio} onChange={e => onSelectAudio(e)} style={{display: 'none'}}/>
<div onClick={() => attachAudio.current.click()}>Attach Audio</div>
</Menu.Item>
<Menu.Item key="2">
<div>Add Audio</div>
<input type='file' name="asset" accept="video/*" ref={attachVideo} onChange={e => onSelectVideo(e)} style={{display: 'none'}}/>
<div onClick={() => attachVideo.current.click()}>Attach Video</div>
</Menu.Item>
</Menu>
);
return (
<AddTopicWrapper>
<div class="carousel"></div>
<div class="carousel">
<Carousel ready={true} items={state.assets} itemRenderer={renderItem} itemRemove={removeItem} />
</div>
<div class="message">
<Input.TextArea ref={msg} placeholder="New Message" spellCheck="true" autoSize={{ minRows: 2, maxRows: 6 }}
autocapitalize="none" enterkeyhint="send" onKeyDown={(e) => keyDown(e)} />

View File

@ -28,8 +28,8 @@ export const AddTopicWrapper = styled.div`
flex-align: center;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
width: 36px;
height: 36px;
cursor: pointer;
border: 1px solid ${Colors.divider};
background-color: ${Colors.white};

View File

@ -0,0 +1,32 @@
import React, { useEffect, useState } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import { SoundOutlined } from '@ant-design/icons';
import { AudioFileWrapper, LabelInput } from './AudioFile.styled';
export function AudioFile({ onLabel }) {
const [state, setState] = useState({ height: 0 });
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
return (
<AudioFileWrapper>
<ReactResizeDetector handleWidth={false} handleHeight={true}>
{({ height }) => {
if (height != state.height) {
updateState({ height });
}
return (
<div class="square" style={{ width: state.height }}>
<SoundOutlined style={{ fontSize: 32, color: '#eeeeee' }} />
<LabelInput placeholder="Label" bordered={false} onChange={(e) => onLabel(e.target.value)}/>;
</div>
)
}}
</ReactResizeDetector>
</AudioFileWrapper>
)
}

View File

@ -0,0 +1,24 @@
import styled from 'styled-components';
import { Input } from 'antd';
export const AudioFileWrapper = styled.div`
position: relative;
height: 100%;
.square {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #444444;
}
`;
export const LabelInput = styled(Input)`
position: absolute;
width: 100%;
bottom: 0;
text-align: center;
color: white;
`

View File

@ -1,16 +1,100 @@
import { useContext, useState } from 'react';
import { CardContext } from 'context/CardContext';
import { ChannelContext } from 'context/ChannelContext';
export function useAddTopic() {
export function useAddTopic(cardId, channelId) {
const [state, setState] = useState({});
const [state, setState] = useState({
assets: [],
messageText: null,
textColor: '#444444',
textColorSet: false,
textSize: 14,
textSizeSet: false,
busy: false,
});
const card = useContext(CardContext);
const channel = useContext(ChannelContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
};
const addAsset = (value) => {
setState((s) => {
let assets = [...s.assets, value];
return { ...s, assets };
});
}
const updateAsset = (index, value) => {
setState((s) => {
s.assets[index] = { ...s.assets[index], ...value };
return { ...s };
});
}
const removeAsset = (index) => {
setState((s) => {
s.assets.splice(index, 1);
let assets = [...s.assets];
return { ...s, assets };
});
}
const actions = {
setTextColor: (textColor) => {
updateState({ textColor });
addImage: (image) => {
let url = URL.createObjectURL(image);
addAsset({ image, url })
},
addVideo: (video) => {
let url = URL.createObjectURL(video);
addAsset({ video, url, position: 0 })
},
addAudio: (audio) => {
let url = URL.createObjectURL(audio);
addAsset({ audio, url, label: '' })
},
setLabel: (index, label) => {
updateAsset(index, { label });
},
setPosition: (index, position) => {
updateAsset(index, { position });
},
removeAsset: (idx) => { removeAsset(idx) },
setTextColor: (value) => {
updateState({ textColorSet: true, textColor: value });
},
setMessageText: (value) => {
updateState({ messageText: value });
},
setTextSize: (value) => {
updateState({ textSizeSet: true, textSize: value });
},
addTopic: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
let message = {
text: state.messageText,
textColor: state.textColorSet ? state.textColor : null,
textSize: state.textSizeSet ? state.textSize : null,
};
if (cardId) {
await card.actions.addChannelTopic(cardId, channelId, message, state.assets);
}
else {
await channel.actions.addChannelTopic(channelId, message, state.assets);
}
updateState({ messageText: null, textColor: '#444444', textColorSet: false, textSize: 12, textSizeSet: false, assets: [] });
}
catch(err) {
console.log(err);
window.alert("failed to add message");
}
updateState({ busy: false });
}
},
};

View File

@ -0,0 +1,66 @@
import React, { useEffect, useState, useRef } from 'react';
import ReactPlayer from 'react-player'
import ReactResizeDetector from 'react-resize-detector';
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
import { VideoFileWrapper, LabelInput } from './VideoFile.styled';
export function VideoFile({ url, onPosition }) {
const [state, setState] = useState({ width: 0, height: 0 });
const [playing, setPlaying] = useState(false);
const player = useRef(null);
const seek = useRef(0);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const onSeek = (offset) => {
if (player.current) {
let len = player.current.getDuration();
if (len > 128) {
offset *= Math.floor(len / 128);
}
seek.current += offset;
if (seek.current < 0 || seek.current >= len) {
seek.current = 0;
}
onPosition(seek.current);
player.current.seekTo(seek.current, 'seconds');
setPlaying(true);
}
}
const onPause = () => {
setPlaying(false);
}
return (
<VideoFileWrapper>
<ReactResizeDetector handleWidth={true} handleHeight={true}>
{({ width, height }) => {
if (width != state.width || height != state.height) {
updateState({ width, height });
}
return <ReactPlayer ref={player} playing={playing} playbackRate={0} controls={false} height="100%" width="auto" url={url}
onStart={() => onPause()} onPlay={() => onPause()} />
}}
</ReactResizeDetector>
<div class="overlay" style={{ width: state.width, height: state.height }}>
<div class="arrows">
<div class="left-arrow">
<div class="icon" onClick={() => onSeek(-1)}>
<LeftOutlined style={{ fontSize: 32, color: '#eeeeee' }} />
</div>
</div>
<div class="right-arrow">
<div class="icon" onClick={() => onSeek(1)}>
<RightOutlined style={{ fontSize: 32, color: '#eeeeee' }} />
</div>
</div>
</div>
</div>
</VideoFileWrapper>
)
}

View File

@ -0,0 +1,39 @@
import styled from 'styled-components';
export const VideoFileWrapper = styled.div`
position: relative;
height: 100%;
.overlay {
position: absolute;
top: 0;
height: 100%;
display: flex;
align-items: center;
.arrows {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.left-arrow {
width: 50%;
display: flex;
justify-content: flex-begin;
}
.right-arrow {
width: 50%;
display: flex;
justify-content: flex-end;
}
.icon {
cursor: pointer;
}
}
}
`;