add class to layout, add scrolltobottom, add newmessage, refresh on newmessage, store channel
This commit is contained in:
parent
599896ed57
commit
1e123c7b74
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -2,12 +2,19 @@ import LeftPanel from "./LeftPanel"
|
||||||
import { Pb } from "../EspaceMembres"
|
import { Pb } from "../EspaceMembres"
|
||||||
import { useContext, useEffect, useState } from "preact/hooks"
|
import { useContext, useEffect, useState } from "preact/hooks"
|
||||||
import Message from "./Chat/Message"
|
import Message from "./Chat/Message"
|
||||||
|
import NewMessage from "./Chat/NewMessage"
|
||||||
import type { MessageExpand } from "./Chat/Message"
|
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() {
|
function Chat() {
|
||||||
const pb = useContext(Pb);
|
const pb = useContext(Pb);
|
||||||
const [messages, setMessages] = useState<MessageExpand[]>([]);
|
const [messages, setMessages] = useState<MessageExpand[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getMessages() {
|
async function getMessages() {
|
||||||
const data = await pb.collection('messages').getFullList<MessageExpand>({
|
const data = await pb.collection('messages').getFullList<MessageExpand>({
|
||||||
|
|
@ -17,23 +24,49 @@ function Chat() {
|
||||||
setMessages(data);
|
setMessages(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('newMessage', getMessages);
|
||||||
|
|
||||||
getMessages();
|
getMessages();
|
||||||
}, [pb]);
|
}, [pb]);
|
||||||
|
|
||||||
return (
|
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">
|
||||||
{messages.map((message) => (
|
<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">
|
||||||
<Message message={message} />
|
{messages.map((message) => (
|
||||||
))}
|
<Message message={message} />
|
||||||
|
))}
|
||||||
|
</ScrollToBottom>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<NewMessage />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
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 (
|
return (
|
||||||
<div className="flex min-h-full flex-grow">
|
<div className="flex min-h-full max-h-[90vh] flex-grow overflow-y-hidden">
|
||||||
<LeftPanel />
|
<CurrentChannel.Provider value={{ get: () => channel, set: setChannel }}>
|
||||||
<Chat />
|
<LeftPanel />
|
||||||
|
<Chat />
|
||||||
|
</CurrentChannel.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,26 @@
|
||||||
import { FaCog } from "react-icons/fa";
|
import { FaCog } from "react-icons/fa";
|
||||||
import { Pb } from "../../EspaceMembres";
|
import { Pb } from "../../EspaceMembres";
|
||||||
|
import { CurrentChannel } from "../Home";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
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() {
|
export default function Channels() {
|
||||||
const pb = useContext(Pb);
|
const pb = useContext(Pb);
|
||||||
const [channels, setChannels] = useState<Array<ChannelsRecord>>([]);
|
const { set } = useContext(CurrentChannel);
|
||||||
|
|
||||||
|
const [channels, setChannels] = useState<Array<ChannelsResponse>>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pb.collection('channels').getFullList<ChannelsRecord>().then((data) => {
|
pb.collection('channels').getFullList<ChannelsResponse>().then((data) => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
setChannels(data);
|
setChannels(data);
|
||||||
|
set({ name: data[0].name ?? '', id: data[0].id ?? ''});
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const selectChannel = (channel: { name: string, id: string }) => {
|
||||||
|
set(channel);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-grow border-b-2 border-b-gray-200">
|
<div className="flex flex-col flex-grow border-b-2 border-b-gray-200">
|
||||||
<div className="flex justify-between items-center p-4">
|
<div className="flex justify-between items-center p-4">
|
||||||
|
|
@ -23,7 +31,7 @@ export default function Channels() {
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{channels.map((channel) => (
|
{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">
|
<p className="flex-grow">
|
||||||
<span className="text-gray-400">#</span> {channel.name}
|
<span className="text-gray-400">#</span> {channel.name}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import Footer from '../components/Footer.astro';
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
footer?: boolean;
|
footer?: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, footer = true } = Astro.props;
|
const { title, footer = true, className } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
@ -20,7 +21,7 @@ const { title, footer = true } = Astro.props;
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-[100vh] flex flex-col">
|
<body class={"min-h-[100vh] flex flex-col" + (className ? ` ${className}` : '')}>
|
||||||
<Header />
|
<Header />
|
||||||
<div class="flex flex-col flex-grow">
|
<div class="flex flex-col flex-grow">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import EspaceMembres from '../components/EspaceMembres.tsx';
|
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"/>
|
<EspaceMembres client:only="preact"/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue