Compare commits
18 Commits
test-actio
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
c97a54dfd9 | |
|
|
1e123c7b74 | |
|
|
599896ed57 | |
|
|
78ef06fbae | |
|
|
a6351250cf | |
|
|
307650b5c4 | |
|
|
96e703cfac | |
|
|
fa55018735 | |
|
|
fd9f4cac65 | |
|
|
bec2d73114 | |
|
|
a66bfd2474 | |
|
|
ab43920e4f | |
|
|
eb001a4ea8 | |
|
|
d7b1d8d77d | |
|
|
be7c0be27c | |
|
|
265d16b628 | |
|
|
775472d66c | |
|
|
eeb4913e0d |
|
|
@ -0,0 +1,5 @@
|
|||
PUBLIC_PB_API=https://example.com
|
||||
|
||||
PB_TYPEGEN_URL='https://example.com'
|
||||
PB_TYPEGEN_EMAIL='pb@example.com'
|
||||
PB_TYPEGEN_PASSWORD='password'
|
||||
|
|
@ -4,8 +4,6 @@ on:
|
|||
push:
|
||||
tags:
|
||||
- 'v*.*'
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -14,10 +12,14 @@ jobs:
|
|||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build pchl using Dockerfile
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: false
|
||||
tags: pchl:${{ gitea.tag }}
|
||||
load: true
|
||||
tags: pchl:${{ gitea.ref_name }},pchl:${{ gitea.sha }}
|
||||
|
|
|
|||
|
|
@ -20,3 +20,5 @@ pnpm-debug.log*
|
|||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
.cosine
|
||||
10
package.json
10
package.json
|
|
@ -7,16 +7,18 @@
|
|||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
"astro": "astro",
|
||||
"typegen": "pocketbase-typegen --out ./src/types/pb_types.d.ts --env"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/preact": "^3.0.0",
|
||||
"@astrojs/tailwind": "^5.0.0",
|
||||
"astro": "^3.0.12",
|
||||
"pocketbase": "^0.18.0",
|
||||
"preact": "^10.6.5",
|
||||
"pocketbase-typegen": "^1.2.1",
|
||||
"preact": "^10.17.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"sharp": "^0.32.5",
|
||||
"tailwindcss": "^3.0.24"
|
||||
"tailwindcss": "^3.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1104
pnpm-lock.yaml
1104
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 337 KiB |
|
|
@ -4,7 +4,9 @@ import PocketBase from 'pocketbase';
|
|||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import Home from "./Members/Home";
|
||||
|
||||
const pb = new PocketBase(import.meta.env.PUBLIC_PB_API);
|
||||
export const pbUrl = import.meta.env.PUBLIC_PB_API;
|
||||
|
||||
const pb = new PocketBase(pbUrl);
|
||||
export const Pb = createContext(pb);
|
||||
|
||||
export const Router = createContext({
|
||||
|
|
|
|||
|
|
@ -3,36 +3,113 @@ import { Image } from "astro:assets"
|
|||
import logo from "../assets/logo.png"
|
||||
---
|
||||
|
||||
|
||||
<header class="flex w-full items-center justify-between bg-gray-800 p-8 text-white">
|
||||
<a href="/">
|
||||
<Image src={logo} alt="Logo Photo Club Haute Lozère" width={128} height={77} />
|
||||
</a>
|
||||
<nav>
|
||||
<ul class="flex items-center gap-2 space-x-4">
|
||||
<li>
|
||||
<a href="/le-club" class="font-poppins text-xl">
|
||||
<header class="flex w-full items-center justify-between bg-gray-800 p-8 text-white">
|
||||
<a href="/">
|
||||
<Image src={logo} alt="Logo Photo Club Haute Lozère" width={128} height={77} />
|
||||
</a>
|
||||
<nav>
|
||||
<div class="flex md:hidden">
|
||||
<button type="button" class="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-200" id="menu-open">
|
||||
<span class="sr-only">Ouvrir le menu</span>
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<ul class="flex items-center gap-2 space-x-4">
|
||||
<li>
|
||||
<a href="/le-club" class="font-poppins text-xl">
|
||||
Le club
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/galeries" class="font-poppins text-xl">
|
||||
Galeries
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact" class="font-poppins text-xl">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/espace-membres"
|
||||
class="bg-white px-6 py-2 font-poppins text-xl text-stone-900"
|
||||
>
|
||||
Espace membres
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="hidden" role="dialog" aria-modal="true" style="display: none" id="mobile-menu">
|
||||
<div class="fixed inset-0 z-10" id="menu-click-over"></div>
|
||||
<div class="fixed right-0 z-10 w-full overflow-y-auto bg-white px-6 py-6
|
||||
sm:max-w-sm ring-1 ring-gray-900/10 sm:right-4 rounded-md">
|
||||
<div class="flex items-center justify-end">
|
||||
<button type="button" class="-m-2.5 rounded-md p-2.5 text-gray-700" id="menu-close">
|
||||
<span class="sr-only">Fermer</span>
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<nav class="grid gap-6 text-gray-700">
|
||||
<a href="/le-club" class="-m-3 p-3 flex items-center rounded-lg hover:bg-gray-50">
|
||||
<div class="ml-4 text-base font-medium">
|
||||
Le club
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/galeries" class="font-poppins text-xl">
|
||||
</div>
|
||||
</a>
|
||||
<a href="/galeries" class="-m-3 p-3 flex items-center rounded-lg hover:bg-gray-50">
|
||||
<div class="ml-4 text-base font-medium">
|
||||
Galeries
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact" class="font-poppins text-xl">
|
||||
</div>
|
||||
</a>
|
||||
<a href="/contact" class="-m-3 p-3 flex items-center rounded-lg hover:bg-gray-50">
|
||||
<div class="ml-4 text-base font-medium">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/espace-membres"
|
||||
class="bg-white px-6 py-2 font-poppins text-xl text-stone-900"
|
||||
>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/espace-membres" class="-m-3 p-3 flex items-center rounded-lg hover:bg-gray-50">
|
||||
<div class="ml-4 text-base font-medium">
|
||||
Espace membres
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
<script>
|
||||
const menu = document.querySelector('#mobile-menu')
|
||||
const menuOpen = document.querySelector('#menu-open')
|
||||
const menuClose = document.querySelector('#menu-close')
|
||||
if (!menu || !menuOpen || !menuClose) {
|
||||
throw new Error('Missing menu elements')
|
||||
}
|
||||
|
||||
menuOpen.addEventListener('click', () => {
|
||||
menu.setAttribute('style', 'display: block')
|
||||
menuOpen.setAttribute('style', 'display: none')
|
||||
})
|
||||
|
||||
menuClose.addEventListener('click', () => {
|
||||
menu.setAttribute('style', 'display: none')
|
||||
menuOpen.setAttribute('style', 'display: block')
|
||||
})
|
||||
|
||||
const menuClickOver = document.querySelector('#menu-click-over')
|
||||
if (!menuClickOver) {
|
||||
throw new Error('Missing menu click over element')
|
||||
}
|
||||
document.addEventListener('click', (event) => {
|
||||
if (event.target === menuClickOver) {
|
||||
menu.setAttribute('style', 'display: none')
|
||||
menuOpen.setAttribute('style', 'display: block')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default function Login() {
|
|||
<div>
|
||||
<label for="email" className="block text-sm font-medium leading-6 text-gray-900">Adresse e-mail</label>
|
||||
<div className="mt-2">
|
||||
<input id="email" name="email" type="email" autocomplete="email" required className="block w-full rounded-sm border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6" />
|
||||
<input id="email" name="email" type="email" autocomplete="email" required className="block w-full rounded-sm border-0 p-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export default function Login() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required className="block w-full rounded-sm border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6" />
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required className="block w-full rounded-sm border-0 p-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 sm:text-sm sm:leading-6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import type { UsersResponse } from "../../types/pb_types";
|
||||
|
||||
type AvatarProps = {
|
||||
avatarUrl?: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
export default function Avatar({avatarUrl, firstName, lastName}: AvatarProps) {
|
||||
if (avatarUrl !== undefined) {
|
||||
return (
|
||||
<img src={avatarUrl} alt={`${firstName} ${lastName}`} className="rounded-full border-2 border-gray-200 h-full" />
|
||||
)
|
||||
}
|
||||
|
||||
const avatar = `https://api.dicebear.com/7.x/adventurer/svg?flip=true&seed=${firstName}+${lastName}`;
|
||||
|
||||
return (
|
||||
<img src={avatar} alt="Profile" className="rounded-full border-2 border-gray-200 h-20" />
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import type { MessagesResponse, UsersResponse } from "../../../types/pb_types";
|
||||
import Avatar from "../Avatar";
|
||||
import { FaRegClock } from "react-icons/fa";
|
||||
|
||||
export type MessageExpand = MessagesResponse<{
|
||||
author: UsersResponse;
|
||||
}>;
|
||||
|
||||
|
||||
export default function Message({ message }: { message: MessageExpand }) {
|
||||
if (!message.expand) return null;
|
||||
const author = message.expand.author;
|
||||
const avatarUrl = author.avatar ? import.meta.env.PUBLIC_PB_API + `/api/files/${author.collectionId}/${author.id}/${author.avatar}?thumb=100x100` : undefined;
|
||||
const date = new Date(message.created);
|
||||
const dateStr = `${date.getHours()}:${date.getMinutes()}`;
|
||||
|
||||
return (
|
||||
<div className='flex justify-start p-4' key={message.id}>
|
||||
{message.expand && <Avatar avatarUrl={avatarUrl} firstName={author.firstname} lastName={author.lastname} />}
|
||||
<div className='flex flex-col items-start justify-center'>
|
||||
<div className='flex items-center my-4 ml-2 gap-2'>
|
||||
<p className="pr-2 py-2 border-b border-gray-200 text-xl text-gray-600">{author.firstname} {author.lastname}</p>
|
||||
<p className="flex gap-2 items-center"><FaRegClock />{dateStr}</p>
|
||||
</div>
|
||||
<div className='p-2 bg-gray-100 rounded-md '>
|
||||
<p>{message.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,17 +1,72 @@
|
|||
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>({
|
||||
expand: 'author,users(avatar)'
|
||||
});
|
||||
if (!data) return;
|
||||
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">
|
||||
<LeftPanel />
|
||||
<Chat />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,25 @@
|
|||
import { FaCog } from "react-icons/fa";
|
||||
import { Pb } from "../../EspaceMembres";
|
||||
import { CurrentChannel } from "../Home";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import type { ChannelsResponse } from "../../../types/pb_types";
|
||||
|
||||
export default function Channels() {
|
||||
const channels = ["Discussion", "Nature", "Portrait", "Architecture"];
|
||||
const pb = useContext(Pb);
|
||||
const { set } = useContext(CurrentChannel);
|
||||
|
||||
const [channels, setChannels] = useState<Array<ChannelsResponse>>([]);
|
||||
useEffect(() => {
|
||||
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">
|
||||
|
|
@ -13,9 +31,9 @@ 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}
|
||||
<span className="text-gray-400">#</span> {channel.name}
|
||||
</p>
|
||||
<FaCog className="transition hover:text-gray-700" />
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
import { useContext } from "preact/hooks";
|
||||
import { Pb } from "../../EspaceMembres"
|
||||
import type { UsersRecord } from "../../../types/pb_types";
|
||||
|
||||
export default function User() {
|
||||
const pb = useContext(Pb);
|
||||
const {firstname, lastname, avatar}: UsersRecord = pb.authStore.model || {};
|
||||
const {id} = pb.authStore.model || {};
|
||||
const avatarUrl = import.meta.env.PUBLIC_PB_API + `/api/files/_pb_users_auth_/${id}/${avatar}?thumb=100x100`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center p-4 border-b-2 border-gray-200 max-h-[6rem]">
|
||||
<img src="https://placekitten.com/200/200" alt="Profile" className="rounded-full border-2 border-gray-200 h-full" />
|
||||
<p className="ml-2 text-center flex-grow">John Doe</p>
|
||||
<img src={avatarUrl} alt="Profile" className="rounded-full border-2 border-gray-200 h-full" />
|
||||
<p className="ml-2 text-center flex-grow capitalize">{firstname} {lastname}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import { Image } from 'astro:assets';
|
||||
import forum from "../assets/forum-eaee.png";
|
||||
import autreForum from "../assets/132108978_o.png";
|
||||
import forum from "../assets/forum-1.png";
|
||||
import autreForum from "../assets/forum-2.png";
|
||||
import banner from "../assets/banner.png";
|
||||
---
|
||||
|
||||
<Layout title="Accueil">
|
||||
<main>
|
||||
<Image src={banner} alt="Photo club haute lozère" width={1920} height={1080} class="w-full" />
|
||||
<div class="container mx-auto p-8">
|
||||
<div class="mb-4 flex gap-10">
|
||||
<div class="w-1/2">
|
||||
<h1 class="font-poppins text-5xl">Notre club photo</h1>
|
||||
<div class="mb-4">
|
||||
<div class="w-full md:w-1/2">
|
||||
<h1 class="font-poppins text-5xl mb-4">Notre club photo</h1>
|
||||
<div class="line h-1 w-full bg-black"></div>
|
||||
</div>
|
||||
<div class="w-1/2"></div>
|
||||
</div>
|
||||
<div class="flex gap-10">
|
||||
<div class="flex w-1/2 flex-col gap-8">
|
||||
<div class="flex gap-10 flex-col md:flex-row">
|
||||
<div class="flex w-full md:w-1/2 flex-col gap-8">
|
||||
<p class="text-xl">
|
||||
Le Photo Club de Haute Lozère est une association loi 1901 créée
|
||||
en 2018.
|
||||
|
|
@ -35,7 +36,7 @@ import autreForum from "../assets/132108978_o.png";
|
|||
</p>
|
||||
<Image src={forum} alt="Forum photo club haute lozère" width={740} height={320} />
|
||||
</div>
|
||||
<div class="flex w-1/2 flex-col gap-6">
|
||||
<div class="flex w-full md:w-1/2 flex-col gap-8">
|
||||
<Image src={autreForum} alt="Forum photo club haute lozère" width={740} height={320} />
|
||||
<p class="text-xl">
|
||||
Ainsi, des sorties à thème sont programmées avec des séances
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import { Image } from "astro:assets";
|
||||
import forum1 from "../assets/forum-1.png";
|
||||
import forum2 from "../assets/forum-2.png";
|
||||
---
|
||||
|
||||
<Layout title="Le club">
|
||||
<div class="container mx-auto p-8">
|
||||
<div class="flex flex-col gap-6">
|
||||
<h1 class="text-4xl font-bold">Le club</h1>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="mb-4">
|
||||
<div class="w-full md:w-1/2">
|
||||
<h1 class="font-poppins text-5xl mb-4">Le club</h1>
|
||||
<div class="line h-1 w-full bg-black"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-10 flex-col md:flex-row">
|
||||
<div class="flex w-full md:w-1/2 flex-col gap-8">
|
||||
<p class="text-xl">
|
||||
Le club photo de la Haute Lozère a été créé en 1982 par un groupe
|
||||
d’amateurs de photographie. Il a été reconnu d’utilité publique en
|
||||
|
|
@ -26,20 +34,10 @@ import Layout from "../layouts/Layout.astro";
|
|||
photographique) travaux informatiques de post traitement font parties
|
||||
de nos activités.
|
||||
</p>
|
||||
<img
|
||||
src={"/forum-eaee.png"}
|
||||
alt="Forum photo club haute lozère"
|
||||
width={740}
|
||||
height={320}
|
||||
/>
|
||||
<Image src={forum1} alt="Forum photo club haute lozère" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<img
|
||||
src={"/132108978_o.png"}
|
||||
alt="Forum photo club haute lozère"
|
||||
width={740}
|
||||
height={320}
|
||||
/>
|
||||
<div class="flex w-full md:w-1/2 flex-col gap-8">
|
||||
<Image src={forum2} alt="Forum photo club haute lozère" />
|
||||
<p class="text-xl">
|
||||
Ainsi, des sorties à thème sont programmées avec des séances
|
||||
d’analyse des photos réalisées.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* This file was @generated using pocketbase-typegen
|
||||
*/
|
||||
|
||||
import type PocketBase from 'pocketbase'
|
||||
import type { RecordService } from 'pocketbase'
|
||||
|
||||
export enum Collections {
|
||||
Albums = "albums",
|
||||
Channels = "channels",
|
||||
Messages = "messages",
|
||||
Pages = "pages",
|
||||
Photos = "photos",
|
||||
Users = "users",
|
||||
}
|
||||
|
||||
// Alias types for improved usability
|
||||
export type IsoDateString = string
|
||||
export type RecordIdString = string
|
||||
export type HTMLString = string
|
||||
|
||||
// System fields
|
||||
export type BaseSystemFields<T = never> = {
|
||||
id: RecordIdString
|
||||
created: IsoDateString
|
||||
updated: IsoDateString
|
||||
collectionId: string
|
||||
collectionName: Collections
|
||||
expand?: T
|
||||
}
|
||||
|
||||
export type AuthSystemFields<T = never> = {
|
||||
email: string
|
||||
emailVisibility: boolean
|
||||
username: string
|
||||
verified: boolean
|
||||
} & BaseSystemFields<T>
|
||||
|
||||
// Record types for each collection
|
||||
|
||||
export type AlbumsRecord = {
|
||||
description?: string
|
||||
pictures?: RecordIdString[]
|
||||
title?: string
|
||||
}
|
||||
|
||||
export type ChannelsRecord = {
|
||||
creator?: RecordIdString
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type MessagesRecord = {
|
||||
attachment?: string
|
||||
author?: RecordIdString
|
||||
channel?: RecordIdString
|
||||
content?: string
|
||||
}
|
||||
|
||||
export type PagesRecord<Tcontent = unknown> = {
|
||||
content?: null | Tcontent
|
||||
slug?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export type PhotosRecord = {
|
||||
alt?: string
|
||||
description?: string
|
||||
image?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export type UsersRecord = {
|
||||
avatar?: string
|
||||
firstname?: string
|
||||
lastname?: string
|
||||
}
|
||||
|
||||
// Response types include system fields and match responses from the PocketBase API
|
||||
export type AlbumsResponse<Texpand = unknown> = Required<AlbumsRecord> & BaseSystemFields<Texpand>
|
||||
export type ChannelsResponse<Texpand = unknown> = Required<ChannelsRecord> & BaseSystemFields<Texpand>
|
||||
export type MessagesResponse<Texpand = unknown> = Required<MessagesRecord> & BaseSystemFields<Texpand>
|
||||
export type PagesResponse<Tcontent = unknown, Texpand = unknown> = Required<PagesRecord<Tcontent>> & BaseSystemFields<Texpand>
|
||||
export type PhotosResponse<Texpand = unknown> = Required<PhotosRecord> & BaseSystemFields<Texpand>
|
||||
export type UsersResponse<Texpand = unknown> = Required<UsersRecord> & AuthSystemFields<Texpand>
|
||||
|
||||
// Types containing all Records and Responses, useful for creating typing helper functions
|
||||
|
||||
export type CollectionRecords = {
|
||||
albums: AlbumsRecord
|
||||
channels: ChannelsRecord
|
||||
messages: MessagesRecord
|
||||
pages: PagesRecord
|
||||
photos: PhotosRecord
|
||||
users: UsersRecord
|
||||
}
|
||||
|
||||
export type CollectionResponses = {
|
||||
albums: AlbumsResponse
|
||||
channels: ChannelsResponse
|
||||
messages: MessagesResponse
|
||||
pages: PagesResponse
|
||||
photos: PhotosResponse
|
||||
users: UsersResponse
|
||||
}
|
||||
|
||||
// Type for usage with type asserted PocketBase instance
|
||||
// https://github.com/pocketbase/js-sdk#specify-typescript-definitions
|
||||
|
||||
export type TypedPocketBase = PocketBase & {
|
||||
collection(idOrName: 'albums'): RecordService<AlbumsResponse>
|
||||
collection(idOrName: 'channels'): RecordService<ChannelsResponse>
|
||||
collection(idOrName: 'messages'): RecordService<MessagesResponse>
|
||||
collection(idOrName: 'pages'): RecordService<PagesResponse>
|
||||
collection(idOrName: 'photos'): RecordService<PhotosResponse>
|
||||
collection(idOrName: 'users'): RecordService<UsersResponse>
|
||||
}
|
||||
Loading…
Reference in New Issue