add class to layout, add scrolltobottom, add newmessage, refresh on newmessage, store channel

This commit is contained in:
Guillaume Dorce 2024-02-24 18:26:41 +01:00
parent 599896ed57
commit 1e123c7b74
6 changed files with 243 additions and 15 deletions

View File

@ -0,0 +1,119 @@
import { useContext, useRef, useState } from 'preact/hooks';
import { FaPlus, FaTimes } from 'react-icons/fa';
import { MdSend } from 'react-icons/md';
import { Pb } from '../../EspaceMembres';
import { CurrentChannel } from '../Home';
export default function NewMessage() {
const [message, setMessage] = useState('');
const [image, setImage] = useState('');
const pb = useContext(Pb);
const [preview, setPreview] = useState('');
const imageRef = useRef<HTMLInputElement>(null);
const { get } = useContext(CurrentChannel);
const handleImageSelect = (e: any) => {
setImage(e.target.value);
const file = e.target.files[0];
if (file) {
const src = URL.createObjectURL(file);
setPreview(src);
}
};
const handleSubmit = (e: any) => {
e.preventDefault();
if (message.trim().length === 0 && image.trim().length === 0) {
return;
}
const data = new FormData(e.target as HTMLFormElement);
//send message with image to pb
pb.collection('messages').create({
content: message,
image: data.get('image'),
channel: get().id,
author: pb.authStore.model?.id,
}).then(() => {
setMessage('');
setImage('');
setPreview('');
if (imageRef.current) {
imageRef.current.value = '';
}
const event = new Event('newMessage');
document.dispatchEvent(event);
});
};
const handleRemoveImage = () => {
setImage('');
setPreview('');
};
const handleInutChange = (e: any) => {
setMessage(e.target.value);
}
return (
<footer className="new-message z-10 flex justify-center w-full">
<div className="w-full max-w-3xl">
{preview !== '' && (
<div className="image-preview w-fit pb-2 absolute bottom-20">
<img src={preview} alt="Image" className="max-w-xs max-h-48 border-2 border-gray-500" />
<span className="rounded-full text-gray-800 bg-grey-dark text-lg p-2 block absolute -top-5 -right-5 bg-white cursor-pointer border border-slate-800 hover:text-gray-600 hover:border-gray-600 transition-all" onClick={handleRemoveImage}>
<FaTimes className="" />
</span>
</div>
)}
<form
onSubmit={handleSubmit}
className="flex gap-2p-2 border border-gray-500 mx-2 sm:p-3 md:mx-0"
>
<div className="file">
<label htmlFor="image" className="cursor-pointer block p-2">
<span className="rounded-full text-white bg-gray-800 text-lg p-2 block">
<FaPlus className="" />
</span>
<span className="sr-only">Ajouter une image</span>
</label>
<input
type="file"
name="image"
id="image"
accept="image/*"
className="hidden"
{...(image !== '' && { value: image })}
onChange={handleImageSelect}
ref={imageRef}
/>
</div>
<div className="new-message flex-grow">
<input
type="text"
value={message}
onChange={handleInutChange}
placeholder="Écrivez votre message..."
className="text-gray-800 p-2.5 w-full placeholder-gray-400 text-2xl focus:outline-none"
id="content"
name="content"
/>
<label htmlFor="content" className="sr-only">
Écrivez votre message...
</label>
</div>
<button
type="submit"
className="py-2 px-4 text-3xl font-medium text-gray-800 hover:bg-white hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:ring-offset-2"
>
<span className="sr-only">Envoyer</span>
<MdSend className="fill-current" />
</button>
</form>
</div>
</footer>
);
};

View File

@ -0,0 +1,67 @@
import type { ComponentChildren } from 'preact';
import { useCallback, useMemo, useState } from 'preact/hooks';
import { FaChevronDown } from 'react-icons/fa';
export default function ScrollToBottom ({ children, className = '' }: { children: ComponentChildren; className?: string }) {
const [node, setNode] = useState<HTMLDivElement | null>(null);
const [show, setShow] = useState(false);
const bottom = useCallback((node: HTMLDivElement) => {
if (node) {
node.scrollIntoView({ behavior: 'smooth' });
setNode(node);
}
}, []);
useMemo(() => {
if (node) {
const container = document.querySelector('main > div');
container?.addEventListener('DOMNodeInserted', (e) => {
if (!(e.target as Element).classList.contains('message')) return;
if (node?.getBoundingClientRect() && node.getBoundingClientRect().y >= window.innerHeight) {
node?.scrollIntoView({ behavior: 'smooth' });
}
});
container?.querySelectorAll('.message > div > img').forEach((img) => {
img.addEventListener('load', () => {
if (node?.getBoundingClientRect() && node.getBoundingClientRect().y >= window.innerHeight) {
node?.scrollIntoView({ behavior: 'smooth' });
}
});
});
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setShow(false);
} else {
setShow(true);
}
},
{ threshold: 1 }
);
observer.observe(node);
return () => observer.disconnect();
}
}, [node]);
return (
<>
<div className={className}>
{children}
<div className="scroll-bottom" ref={bottom}></div>
</div>
<button
onClick={() => node?.scrollIntoView({ behavior: 'smooth' })}
className={'absolute right-3 ' + (show ? 'animate-show bottom-3 block' : 'animate-hide hidden bottom-0 -mb-10')}
name="bottom"
>
<span className="popup-btn block cursor-pointer rounded-full transition-all border border-gray-500">
<FaChevronDown className="fill-grey-light hover:fill-grey-dark transition-all text-xl w-10 h-10 p-2.5" />
</span>
<span className="sr-only">Descendre en bas de la page</span>
</button>
</>
);
};

View File

@ -2,12 +2,19 @@ import LeftPanel from "./LeftPanel"
import { Pb } from "../EspaceMembres"
import { useContext, useEffect, useState } from "preact/hooks"
import Message from "./Chat/Message"
import NewMessage from "./Chat/NewMessage"
import type { MessageExpand } from "./Chat/Message"
import ScrollToBottom from "./Chat/ScrollToBottom"
import { createContext } from "preact"
export const CurrentChannel = createContext({
get: () => { return { name: '', id: '' } },
set: (channel: { name: string, id: string }) => { }
});
function Chat() {
const pb = useContext(Pb);
const [messages, setMessages] = useState<MessageExpand[]>([]);
useEffect(() => {
async function getMessages() {
const data = await pb.collection('messages').getFullList<MessageExpand>({
@ -17,23 +24,49 @@ function Chat() {
setMessages(data);
}
document.addEventListener('newMessage', getMessages);
getMessages();
}, [pb]);
return (
<div className="w-full">
<div className="w-full min-h-full max-h-full bg-grey flex flex-col justify-between overflow-y-hidden">
<ScrollToBottom className="message-container flex flex-col gap-4 w-full overflow-y-scroll overflow-x-hidden pt-4 pb-6 px-2 md:px-0">
{messages.map((message) => (
<Message message={message} />
))}
</ScrollToBottom>
<div className="flex justify-center">
<div className="w-full max-w-3xl">
<NewMessage />
</div>
</div>
</div>
)
}
export default function Home() {
const [channel, setChannel] = useState({ name: '', id: ''});
useEffect(() => {
const channel = localStorage.getItem('channel');
if (channel) {
setChannel(JSON.parse(channel));
}
}, []);
useEffect(() => {
if (!channel.id) return;
localStorage.setItem('channel', JSON.stringify(channel));
console.log(channel);
}, [channel]);
return (
<div className="flex min-h-full flex-grow">
<div className="flex min-h-full max-h-[90vh] flex-grow overflow-y-hidden">
<CurrentChannel.Provider value={{ get: () => channel, set: setChannel }}>
<LeftPanel />
<Chat />
</CurrentChannel.Provider>
</div>
)
}

View File

@ -1,18 +1,26 @@
import { FaCog } from "react-icons/fa";
import { Pb } from "../../EspaceMembres";
import { CurrentChannel } from "../Home";
import { useContext, useEffect, useState } from "preact/hooks";
import type { ChannelsRecord } from "../../../types/pb_types";
import type { ChannelsResponse } from "../../../types/pb_types";
export default function Channels() {
const pb = useContext(Pb);
const [channels, setChannels] = useState<Array<ChannelsRecord>>([]);
const { set } = useContext(CurrentChannel);
const [channels, setChannels] = useState<Array<ChannelsResponse>>([]);
useEffect(() => {
pb.collection('channels').getFullList<ChannelsRecord>().then((data) => {
pb.collection('channels').getFullList<ChannelsResponse>().then((data) => {
if (!data) return;
setChannels(data);
set({ name: data[0].name ?? '', id: data[0].id ?? ''});
});
}, []);
const selectChannel = (channel: { name: string, id: string }) => {
set(channel);
}
return (
<div className="flex flex-col flex-grow border-b-2 border-b-gray-200">
<div className="flex justify-between items-center p-4">
@ -23,7 +31,7 @@ export default function Channels() {
</div>
<ul>
{channels.map((channel) => (
<li className="p-2 hover:bg-gray-100 cursor-pointer text-xl flex items-center" key={channel}>
<li className="p-2 hover:bg-gray-100 cursor-pointer text-xl flex items-center" key={channel} onClick={() => selectChannel({ name: channel.name ?? '', id: channel.id ?? '' })}>
<p className="flex-grow">
<span className="text-gray-400">#</span> {channel.name}
</p>

View File

@ -5,9 +5,10 @@ import Footer from '../components/Footer.astro';
interface Props {
title: string;
footer?: boolean;
className?: string;
}
const { title, footer = true } = Astro.props;
const { title, footer = true, className } = Astro.props;
---
<!doctype html>
@ -20,7 +21,7 @@ const { title, footer = true } = Astro.props;
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body class="min-h-[100vh] flex flex-col">
<body class={"min-h-[100vh] flex flex-col" + (className ? ` ${className}` : '')}>
<Header />
<div class="flex flex-col flex-grow">
<slot />

View File

@ -2,6 +2,6 @@
import Layout from '../layouts/Layout.astro';
import EspaceMembres from '../components/EspaceMembres.tsx';
---
<Layout title="Espace membres" footer={false}>
<Layout title="Espace membres" footer={false} className='min-h-full max-h-full overflow-y-hidden'>
<EspaceMembres client:only="preact"/>
</Layout>