Compare commits

...

12 Commits
v0.1 ... main

22 changed files with 1670 additions and 104 deletions

5
.env.example Normal file
View File

@ -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'

2
.gitignore vendored
View File

@ -20,3 +20,5 @@ pnpm-debug.log*
# macOS-specific files # macOS-specific files
.DS_Store .DS_Store
.cosine

View File

@ -7,16 +7,18 @@
"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",
"preact": "^10.6.5", "pocketbase-typegen": "^1.2.1",
"preact": "^10.17.1",
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
"sharp": "^0.32.5", "sharp": "^0.32.5",
"tailwindcss": "^3.0.24" "tailwindcss": "^3.3.3"
} }
} }

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 325 KiB

View File

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 337 KiB

View File

@ -4,7 +4,9 @@ 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";
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 Pb = createContext(pb);
export const Router = createContext({ export const Router = createContext({

View File

@ -3,36 +3,113 @@ 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">
<ul class="flex items-center gap-2 space-x-4"> <button type="button" class="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-200" id="menu-open">
<li> <span class="sr-only">Ouvrir le menu</span>
<a href="/le-club" class="font-poppins text-xl"> <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 Le club
</a> </div>
</li> </a>
<li> <a href="/galeries" class="-m-3 p-3 flex items-center rounded-lg hover:bg-gray-50">
<a href="/galeries" class="font-poppins text-xl"> <div class="ml-4 text-base font-medium">
Galeries Galeries
</a> </div>
</li> </a>
<li> <a href="/contact" class="-m-3 p-3 flex items-center rounded-lg hover:bg-gray-50">
<a href="/contact" class="font-poppins text-xl"> <div class="ml-4 text-base font-medium">
Contact Contact
</a> </div>
</li> </a>
<li> <a href="/espace-membres" class="-m-3 p-3 flex items-center rounded-lg hover:bg-gray-50">
<a <div class="ml-4 text-base font-medium">
href="/espace-membres"
class="bg-white px-6 py-2 font-poppins text-xl text-stone-900"
>
Espace membres Espace membres
</a> </div>
</li> </a>
</ul> </nav>
</nav> </div>
</header> </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>

View File

@ -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 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>
</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 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>
</div> </div>

View File

@ -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" />
)
}

View File

@ -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>
)
}

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

@ -1,17 +1,72 @@
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"> <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> </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>
) )
} }

View File

@ -1,7 +1,25 @@
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 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 ( 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">
@ -13,9 +31,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}> <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} <span className="text-gray-400">#</span> {channel.name}
</p> </p>
<FaCog className="transition hover:text-gray-700" /> <FaCog className="transition hover:text-gray-700" />
</li> </li>

View File

@ -1,8 +1,17 @@
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="https://placekitten.com/200/200" alt="Profile" className="rounded-full border-2 border-gray-200 h-full" /> <img src={avatarUrl} alt="Profile" className="rounded-full border-2 border-gray-200 h-full" />
<p className="ml-2 text-center flex-grow">John Doe</p> <p className="ml-2 text-center flex-grow capitalize">{firstname} {lastname}</p>
</div> </div>
) )
} }

View File

@ -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 />

View File

@ -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>

View File

@ -1,22 +1,23 @@
--- ---
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-eaee.png"; import forum from "../assets/forum-1.png";
import autreForum from "../assets/132108978_o.png"; import autreForum from "../assets/forum-2.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 flex gap-10"> <div class="mb-4">
<div class="w-1/2"> <div class="w-full md:w-1/2">
<h1 class="font-poppins text-5xl">Notre club photo</h1> <h1 class="font-poppins text-5xl mb-4">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"> <div class="flex gap-10 flex-col md:flex-row">
<div class="flex w-1/2 flex-col gap-8"> <div class="flex w-full md: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.
@ -35,7 +36,7 @@ import autreForum from "../assets/132108978_o.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-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} /> <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

View File

@ -1,12 +1,20 @@
--- ---
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="flex flex-col gap-6"> <div class="mb-4">
<h1 class="text-4xl font-bold">Le club</h1> <div class="w-full md:w-1/2">
<div class="flex flex-col gap-6"> <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"> <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
damateurs de photographie. Il a été reconnu dutilité publique en damateurs de photographie. Il a été reconnu dutilité publique en
@ -26,20 +34,10 @@ import Layout from "../layouts/Layout.astro";
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>
<img <Image src={forum1} alt="Forum photo club haute lozère" />
src={"/forum-eaee.png"}
alt="Forum photo club haute lozère"
width={740}
height={320}
/>
</div> </div>
<div class="flex flex-col gap-6"> <div class="flex w-full md:w-1/2 flex-col gap-8">
<img <Image src={forum2} alt="Forum photo club haute lozère" />
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
danalyse des photos réalisées. danalyse des photos réalisées.

116
src/types/pb_types.d.ts vendored Normal file
View File

@ -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>
}