Compare commits
No commits in common. "main" and "test-actions" have entirely different histories.
main
...
test-actio
|
|
@ -1,5 +0,0 @@
|
||||||
PUBLIC_PB_API=https://example.com
|
|
||||||
|
|
||||||
PB_TYPEGEN_URL='https://example.com'
|
|
||||||
PB_TYPEGEN_EMAIL='pb@example.com'
|
|
||||||
PB_TYPEGEN_PASSWORD='password'
|
|
||||||
|
|
@ -4,6 +4,8 @@ on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*'
|
- 'v*.*'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -12,14 +14,10 @@ jobs:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
- 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
|
- name: Build pchl using Dockerfile
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
load: true
|
push: false
|
||||||
tags: pchl:${{ gitea.ref_name }},pchl:${{ gitea.sha }}
|
tags: pchl:${{ gitea.tag }}
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,3 @@ pnpm-debug.log*
|
||||||
|
|
||||||
# macOS-specific files
|
# macOS-specific files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
.cosine
|
|
||||||
|
|
@ -7,18 +7,16 @@
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro"
|
||||||
"typegen": "pocketbase-typegen --out ./src/types/pb_types.d.ts --env"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/preact": "^3.0.0",
|
"@astrojs/preact": "^3.0.0",
|
||||||
"@astrojs/tailwind": "^5.0.0",
|
"@astrojs/tailwind": "^5.0.0",
|
||||||
"astro": "^3.0.12",
|
"astro": "^3.0.12",
|
||||||
"pocketbase": "^0.18.0",
|
"pocketbase": "^0.18.0",
|
||||||
"pocketbase-typegen": "^1.2.1",
|
"preact": "^10.6.5",
|
||||||
"preact": "^10.17.1",
|
|
||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
"sharp": "^0.32.5",
|
"sharp": "^0.32.5",
|
||||||
"tailwindcss": "^3.3.3"
|
"tailwindcss": "^3.0.24"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1104
pnpm-lock.yaml
1104
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 337 KiB |
|
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 |
|
|
@ -4,9 +4,7 @@ import PocketBase from 'pocketbase';
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
import Home from "./Members/Home";
|
import Home from "./Members/Home";
|
||||||
|
|
||||||
export const pbUrl = import.meta.env.PUBLIC_PB_API;
|
const pb = new PocketBase(import.meta.env.PUBLIC_PB_API);
|
||||||
|
|
||||||
const pb = new PocketBase(pbUrl);
|
|
||||||
export const Pb = createContext(pb);
|
export const Pb = createContext(pb);
|
||||||
|
|
||||||
export const Router = createContext({
|
export const Router = createContext({
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,12 @@ import { Image } from "astro:assets"
|
||||||
import logo from "../assets/logo.png"
|
import logo from "../assets/logo.png"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
<header class="flex w-full items-center justify-between bg-gray-800 p-8 text-white">
|
<header class="flex w-full items-center justify-between bg-gray-800 p-8 text-white">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<Image src={logo} alt="Logo Photo Club Haute Lozère" width={128} height={77} />
|
<Image src={logo} alt="Logo Photo Club Haute Lozère" width={128} height={77} />
|
||||||
</a>
|
</a>
|
||||||
<nav>
|
<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">
|
<ul class="flex items-center gap-2 space-x-4">
|
||||||
<li>
|
<li>
|
||||||
<a href="/le-club" class="font-poppins text-xl">
|
<a href="/le-club" class="font-poppins text-xl">
|
||||||
|
|
@ -42,74 +34,5 @@ import logo from "../assets/logo.png"
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</nav>
|
</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
|
|
||||||
</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
|
|
||||||
</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
|
|
||||||
</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
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</header>
|
</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>
|
<div>
|
||||||
<label for="email" className="block text-sm font-medium leading-6 text-gray-900">Adresse e-mail</label>
|
<label for="email" className="block text-sm font-medium leading-6 text-gray-900">Adresse e-mail</label>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<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" />
|
<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" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default function Login() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<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" />
|
<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" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
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,72 +1,17 @@
|
||||||
import LeftPanel from "./LeftPanel"
|
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() {
|
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 (
|
return (
|
||||||
<div className="w-full min-h-full max-h-full bg-grey flex flex-col justify-between overflow-y-hidden">
|
<div className="w-full">
|
||||||
<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>
|
</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 max-h-[90vh] flex-grow overflow-y-hidden">
|
<div className="flex min-h-full flex-grow">
|
||||||
<CurrentChannel.Provider value={{ get: () => channel, set: setChannel }}>
|
|
||||||
<LeftPanel />
|
<LeftPanel />
|
||||||
<Chat />
|
<Chat />
|
||||||
</CurrentChannel.Provider>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,7 @@
|
||||||
import { FaCog } from "react-icons/fa";
|
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() {
|
export default function Channels() {
|
||||||
const pb = useContext(Pb);
|
const channels = ["Discussion", "Nature", "Portrait", "Architecture"];
|
||||||
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 (
|
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">
|
||||||
|
|
@ -31,9 +13,9 @@ 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} onClick={() => selectChannel({ name: channel.name ?? '', id: channel.id ?? '' })}>
|
<li className="p-2 hover:bg-gray-100 cursor-pointer text-xl flex items-center" key={channel}>
|
||||||
<p className="flex-grow">
|
<p className="flex-grow">
|
||||||
<span className="text-gray-400">#</span> {channel.name}
|
<span className="text-gray-400">#</span> {channel}
|
||||||
</p>
|
</p>
|
||||||
<FaCog className="transition hover:text-gray-700" />
|
<FaCog className="transition hover:text-gray-700" />
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,8 @@
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
import { Pb } from "../../EspaceMembres"
|
|
||||||
import type { UsersRecord } from "../../../types/pb_types";
|
|
||||||
|
|
||||||
export default function User() {
|
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 (
|
return (
|
||||||
<div className="flex items-center p-4 border-b-2 border-gray-200 max-h-[6rem]">
|
<div className="flex items-center p-4 border-b-2 border-gray-200 max-h-[6rem]">
|
||||||
<img src={avatarUrl} alt="Profile" className="rounded-full border-2 border-gray-200 h-full" />
|
<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 capitalize">{firstname} {lastname}</p>
|
<p className="ml-2 text-center flex-grow">John Doe</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,9 @@ import Footer from '../components/Footer.astro';
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
footer?: boolean;
|
footer?: boolean;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, footer = true, className } = Astro.props;
|
const { title, footer = true } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
@ -21,7 +20,7 @@ const { title, footer = true, className } = 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" + (className ? ` ${className}` : '')}>
|
<body class="min-h-[100vh] flex flex-col">
|
||||||
<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} className='min-h-full max-h-full overflow-y-hidden'>
|
<Layout title="Espace membres" footer={false}>
|
||||||
<EspaceMembres client:only="preact"/>
|
<EspaceMembres client:only="preact"/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,22 @@
|
||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import { Image } from 'astro:assets';
|
import { Image } from 'astro:assets';
|
||||||
import forum from "../assets/forum-1.png";
|
import forum from "../assets/forum-eaee.png";
|
||||||
import autreForum from "../assets/forum-2.png";
|
import autreForum from "../assets/132108978_o.png";
|
||||||
import banner from "../assets/banner.png";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Accueil">
|
<Layout title="Accueil">
|
||||||
<main>
|
<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="container mx-auto p-8">
|
||||||
<div class="mb-4">
|
<div class="mb-4 flex gap-10">
|
||||||
<div class="w-full md:w-1/2">
|
<div class="w-1/2">
|
||||||
<h1 class="font-poppins text-5xl mb-4">Notre club photo</h1>
|
<h1 class="font-poppins text-5xl">Notre club photo</h1>
|
||||||
<div class="line h-1 w-full bg-black"></div>
|
<div class="line h-1 w-full bg-black"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-1/2"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-10 flex-col md:flex-row">
|
<div class="flex gap-10">
|
||||||
<div class="flex w-full md:w-1/2 flex-col gap-8">
|
<div class="flex w-1/2 flex-col gap-8">
|
||||||
<p class="text-xl">
|
<p class="text-xl">
|
||||||
Le Photo Club de Haute Lozère est une association loi 1901 créée
|
Le Photo Club de Haute Lozère est une association loi 1901 créée
|
||||||
en 2018.
|
en 2018.
|
||||||
|
|
@ -36,7 +35,7 @@ import banner from "../assets/banner.png";
|
||||||
</p>
|
</p>
|
||||||
<Image src={forum} alt="Forum photo club haute lozère" width={740} height={320} />
|
<Image src={forum} alt="Forum photo club haute lozère" width={740} height={320} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full md:w-1/2 flex-col gap-8">
|
<div class="flex w-1/2 flex-col gap-6">
|
||||||
<Image src={autreForum} alt="Forum photo club haute lozère" width={740} height={320} />
|
<Image src={autreForum} alt="Forum photo club haute lozère" width={740} height={320} />
|
||||||
<p class="text-xl">
|
<p class="text-xl">
|
||||||
Ainsi, des sorties à thème sont programmées avec des séances
|
Ainsi, des sorties à thème sont programmées avec des séances
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,12 @@
|
||||||
---
|
---
|
||||||
import Layout from "../layouts/Layout.astro";
|
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">
|
<Layout title="Le club">
|
||||||
<div class="container mx-auto p-8">
|
<div class="container mx-auto p-8">
|
||||||
<div class="mb-4">
|
<div class="flex flex-col gap-6">
|
||||||
<div class="w-full md:w-1/2">
|
<h1 class="text-4xl font-bold">Le club</h1>
|
||||||
<h1 class="font-poppins text-5xl mb-4">Le club</h1>
|
<div class="flex flex-col gap-6">
|
||||||
<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">
|
<p class="text-xl">
|
||||||
Le club photo de la Haute Lozère a été créé en 1982 par un groupe
|
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
|
d’amateurs de photographie. Il a été reconnu d’utilité publique en
|
||||||
|
|
@ -34,10 +26,20 @@ import forum2 from "../assets/forum-2.png";
|
||||||
photographique) travaux informatiques de post traitement font parties
|
photographique) travaux informatiques de post traitement font parties
|
||||||
de nos activités.
|
de nos activités.
|
||||||
</p>
|
</p>
|
||||||
<Image src={forum1} alt="Forum photo club haute lozère" />
|
<img
|
||||||
|
src={"/forum-eaee.png"}
|
||||||
|
alt="Forum photo club haute lozère"
|
||||||
|
width={740}
|
||||||
|
height={320}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full md:w-1/2 flex-col gap-8">
|
<div class="flex flex-col gap-6">
|
||||||
<Image src={forum2} alt="Forum photo club haute lozère" />
|
<img
|
||||||
|
src={"/132108978_o.png"}
|
||||||
|
alt="Forum photo club haute lozère"
|
||||||
|
width={740}
|
||||||
|
height={320}
|
||||||
|
/>
|
||||||
<p class="text-xl">
|
<p class="text-xl">
|
||||||
Ainsi, des sorties à thème sont programmées avec des séances
|
Ainsi, des sorties à thème sont programmées avec des séances
|
||||||
d’analyse des photos réalisées.
|
d’analyse des photos réalisées.
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
/**
|
|
||||||
* 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