Compare commits
No commits in common. "a75de86d68ace375843143acdf32da7d11010ad6" and "5cb71428245f8d945f334448e242d07304a64e99" have entirely different histories.
a75de86d68
...
5cb7142824
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -36,10 +36,7 @@
|
||||||
"api",
|
"api",
|
||||||
"ux",
|
"ux",
|
||||||
"route",
|
"route",
|
||||||
"frontend",
|
"frontend"
|
||||||
"components",
|
|
||||||
"island",
|
|
||||||
"backend"
|
|
||||||
],
|
],
|
||||||
"[ignore]": {
|
"[ignore]": {
|
||||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||||
|
|
|
@ -3,19 +3,16 @@ import { JSX } from 'preact'
|
||||||
type Units = 'rem' | '%' | 'px'
|
type Units = 'rem' | '%' | 'px'
|
||||||
|
|
||||||
export function AutoGrid(
|
export function AutoGrid(
|
||||||
{ columnWidth, children, style }: {
|
{ columnWidth, children }: {
|
||||||
columnWidth: `${number}${Units}`
|
columnWidth: `${number}${Units}`
|
||||||
children: JSX.Element | JSX.Element[]
|
children: JSX.Element | JSX.Element[]
|
||||||
style?: JSX.CSSProperties
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class='components__auto_grid'
|
class='components__auto_grid'
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns:
|
gridTemplateColumns: `repeat(auto-fit, minmax(${columnWidth}, 1fr));`,
|
||||||
`repeat(auto-fit, minmax(${columnWidth}, 1fr));`,
|
|
||||||
...style,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,164 +0,0 @@
|
||||||
.components__blog_block {
|
|
||||||
min-width: 10rem;
|
|
||||||
aspect-ratio: 3 / 4;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow: 0 0 0.4rem 0.2rem var(--_translucent);
|
|
||||||
border: var(--_border-size) solid transparent;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 80%;
|
|
||||||
background-position: center var(--_gap);
|
|
||||||
backdrop-filter: blur(var(--_blur));
|
|
||||||
background-color: var(--_background-color);
|
|
||||||
|
|
||||||
&:has(a:focus-visible),
|
|
||||||
&:hover {
|
|
||||||
border: var(--_border-size) solid var(--_accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
& h3 {
|
|
||||||
margin: 0;
|
|
||||||
padding: var(--_gap) var(--_gap-half);
|
|
||||||
backdrop-filter: blur(var(--_blur));
|
|
||||||
background-color: var(--_translucent);
|
|
||||||
border-bottom: 1px solid currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
& a {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block--card {
|
|
||||||
max-width: 20rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block--placeholder {
|
|
||||||
animation: var(--animation-blink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block--fallback {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(.components__blog_block--placeholder, .components__blog_block--fallback) {
|
|
||||||
h3 {
|
|
||||||
flex-grow: 1;
|
|
||||||
border: none;
|
|
||||||
align-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block__spacer {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block--card .components__blog_block__spacer {
|
|
||||||
height: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block__links {
|
|
||||||
height: fit-content;
|
|
||||||
display: flex;
|
|
||||||
gap: var(--_gap-half);
|
|
||||||
justify-content: start;
|
|
||||||
padding: var(--_gap-half);
|
|
||||||
backdrop-filter: blur(var(--_blur));
|
|
||||||
background-color: var(--_translucent);
|
|
||||||
border-bottom: 1px solid currentColor;
|
|
||||||
|
|
||||||
& > a::before {
|
|
||||||
content: '🔗';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block__tags {
|
|
||||||
height: fit-content;
|
|
||||||
display: flex;
|
|
||||||
gap: var(--_gap-half);
|
|
||||||
justify-content: start;
|
|
||||||
padding: var(--_gap-half);
|
|
||||||
backdrop-filter: blur(var(--_blur));
|
|
||||||
background-color: var(--_translucent);
|
|
||||||
border-bottom: 1px solid currentColor;
|
|
||||||
|
|
||||||
& > span::before {
|
|
||||||
content: '#';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block__status {
|
|
||||||
display: block;
|
|
||||||
padding: var(--_gap-half);
|
|
||||||
background-color: var(--_translucent);
|
|
||||||
backdrop-filter: blur(var(--_blur));
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block--card .components__blog_block__status {
|
|
||||||
position: absolute;
|
|
||||||
top: var(--_gap-half);
|
|
||||||
left: var(--_gap-half);
|
|
||||||
font-size: larger;
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block__publisher {
|
|
||||||
display: block;
|
|
||||||
padding: var(--_gap-half);
|
|
||||||
background-color: var(--_translucent);
|
|
||||||
backdrop-filter: blur(var(--_blur));
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block--card .components__blog_block__publisher {
|
|
||||||
position: absolute;
|
|
||||||
top: var(--_gap-half);
|
|
||||||
right: var(--_gap-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block__description {
|
|
||||||
text-wrap: balance;
|
|
||||||
flex-grow: 1;
|
|
||||||
backdrop-filter: blur(var(--_blur));
|
|
||||||
padding: var(--_gap-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block--card .components__blog_block__description {
|
|
||||||
min-height: 25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block__body {
|
|
||||||
text-wrap: balance;
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: var(--_gap-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block__footer {
|
|
||||||
height: fit-content;
|
|
||||||
display: flex;
|
|
||||||
gap: var(--_gap);
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: var(--_gap-half);
|
|
||||||
background-color: var(--_background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_post__infos {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--_gap-half);
|
|
||||||
margin-block: var(--_gap-half);
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
border: none;
|
|
||||||
padding: var(--_gap-half);
|
|
||||||
background-color: var(--_translucent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_post__description {
|
|
||||||
margin-block: var(--_gap-half);
|
|
||||||
font-family: var(--_font-family-code);
|
|
||||||
}
|
|
||||||
|
|
||||||
.components__blog_block--post {
|
|
||||||
width: var(--_readable-screen);
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
import { Markdown } from ':components/Markdown.tsx'
|
|
||||||
import { NewsFrontMatter } from ':src/blog/types.ts'
|
|
||||||
|
|
||||||
export type BlogProps = {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
body: string
|
|
||||||
author: string
|
|
||||||
publisher: string
|
|
||||||
lastUpdate: Date
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
hash: string
|
|
||||||
options: NewsFrontMatter['x-cohabit']
|
|
||||||
tags: NewsFrontMatter['tags']
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BlogCard(
|
|
||||||
{ title, description, author, lastUpdate, name, options, tags, publisher }:
|
|
||||||
BlogProps,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class='components__blog_block components__blog_block--card'
|
|
||||||
style={{ backgroundImage: `url(${options.thumbnail})` }}
|
|
||||||
>
|
|
||||||
<div class='components__blog_block__spacer'></div>
|
|
||||||
<h3>
|
|
||||||
<a href={`/blog/${name}`}>{title}</a>
|
|
||||||
</h3>
|
|
||||||
<NewsLinks links={options.links} />
|
|
||||||
<NewsTags tags={tags} />
|
|
||||||
<span class='components__blog_block__publisher'>
|
|
||||||
{`@${publisher}`}
|
|
||||||
</span>
|
|
||||||
<NewsStatus status={options.status} />
|
|
||||||
<div class='components__blog_block__description'>
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
<NewsFooter author={author} lastUpdate={lastUpdate} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BlogPost(
|
|
||||||
{
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
author,
|
|
||||||
lastUpdate,
|
|
||||||
body,
|
|
||||||
url,
|
|
||||||
options,
|
|
||||||
tags,
|
|
||||||
publisher,
|
|
||||||
}: BlogProps,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div class='components__blog_block--post'>
|
|
||||||
<h1>{title}</h1>
|
|
||||||
<div class='components__blog_post__infos'>
|
|
||||||
<span class='components__blog_block__publisher'>
|
|
||||||
{`@${publisher}`}
|
|
||||||
</span>
|
|
||||||
<NewsStatus status={options.status} long />
|
|
||||||
<NewsLinks links={options.links} />
|
|
||||||
<NewsTags tags={tags} />
|
|
||||||
</div>
|
|
||||||
<div class='components__blog_post__infos'>
|
|
||||||
<span>{`Visibilité : ${options.visibility}`}</span>
|
|
||||||
<span>
|
|
||||||
{`Date de délivrance : ${
|
|
||||||
new Date(options.dueDate).toLocaleString()
|
|
||||||
}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class='components__blog_post__description'>
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
<div class='components__blog_post__body'>
|
|
||||||
<Markdown options={{ allowMath: true, baseUrl: url }}>
|
|
||||||
{body}
|
|
||||||
</Markdown>
|
|
||||||
</div>
|
|
||||||
<NewsFooter author={author} lastUpdate={lastUpdate} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NewsTags({ tags }: Pick<BlogProps, 'tags'>) {
|
|
||||||
return (
|
|
||||||
<div class='components__blog_block__tags'>
|
|
||||||
{tags
|
|
||||||
? tags.map((tag) => <span>{tag}</span>)
|
|
||||||
: <span>Aucun tag</span>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NewsFooter(
|
|
||||||
{ author, lastUpdate }: Pick<BlogProps, 'author' | 'lastUpdate'>,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div class='components__blog_block__footer'>
|
|
||||||
<div>
|
|
||||||
<i class='ri-quill-pen-line'></i>
|
|
||||||
<span>{author}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<i class='ri-refresh-line'></i>
|
|
||||||
<span>{lastUpdate.toLocaleDateString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NewsLinks({ links }: Pick<BlogProps['options'], 'links'>) {
|
|
||||||
return (
|
|
||||||
<div class='components__blog_block__links'>
|
|
||||||
{links
|
|
||||||
? links.flatMap(Object.entries).map((
|
|
||||||
[name, link],
|
|
||||||
) => <a href={link} target='_blank' title={name}>{name}</a>)
|
|
||||||
: <span>Aucun lien rapide</span>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NewsStatus(
|
|
||||||
{ status, long = false }: Pick<BlogProps['options'], 'status'> & {
|
|
||||||
long?: boolean
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const title = status === 'canceled'
|
|
||||||
? 'Annulé'
|
|
||||||
: status === 'current'
|
|
||||||
? 'En cours'
|
|
||||||
: status === 'finished'
|
|
||||||
? 'Terminé'
|
|
||||||
: 'Prévu'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
class='components__blog_block__status'
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
{status === 'canceled'
|
|
||||||
? <i class='ri-calendar-close-line'></i>
|
|
||||||
: status === 'current'
|
|
||||||
? <i class='ri-calendar-2-line'></i>
|
|
||||||
: status === 'finished'
|
|
||||||
? <i class='ri-calendar-check-line'></i>
|
|
||||||
: <i class='ri-calendar-2-line'></i>}
|
|
||||||
{long ? ` ${title}` : ''}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
41
components/BlogCard.css
Normal file
41
components/BlogCard.css
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
.components__blog_card {
|
||||||
|
min-width: 10rem;
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--_gap-half);
|
||||||
|
gap: var(--_gap);
|
||||||
|
box-shadow: 0 0 0.4rem 0.2rem var(--_translucent);
|
||||||
|
border: var(--_border-size) solid transparent;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
|
||||||
|
&:has(a:focus-visible),
|
||||||
|
&:hover {
|
||||||
|
border: var(--_border-size) solid var(--_accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
& h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& a {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.components__blog_card__spacer {
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components__blog_card__text {
|
||||||
|
text-wrap: balance;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components__blog_card__footer {
|
||||||
|
height: fit-content;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--_gap);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
54
components/BlogCard.tsx
Normal file
54
components/BlogCard.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
type BlogCardProps = {
|
||||||
|
img: string
|
||||||
|
title: string
|
||||||
|
text: string
|
||||||
|
author: string
|
||||||
|
lasUpdate: Date
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlogCard(
|
||||||
|
{ img, title, text, author, lasUpdate, id }: BlogCardProps,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div class='components__blog_card' style={{ backgroundImage: img }}>
|
||||||
|
<div class='components__blog_card__spacer'></div>
|
||||||
|
<h3>
|
||||||
|
<a href={`/blog/${id}`}>{title}</a>
|
||||||
|
</h3>
|
||||||
|
<div class='components__blog_card__text'>
|
||||||
|
{`${text.slice(0, 150)} ...`}
|
||||||
|
</div>
|
||||||
|
<div class='components__blog_card__footer'>
|
||||||
|
<div>
|
||||||
|
<i class='ri-quill-pen-line'></i>
|
||||||
|
<span>{author}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i class='ri-refresh-line'></i>
|
||||||
|
<span>{lasUpdate.toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text =
|
||||||
|
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui, perferendis enim blanditiis consequatur at porro quod, eligendi alias recusandae modi aliquam non? Quos voluptates quisquam provident animi nisi in ratione.'
|
||||||
|
|
||||||
|
export const blogMock: BlogCardProps[] = Array(50).fill(undefined).map(
|
||||||
|
(_, index) => {
|
||||||
|
return {
|
||||||
|
author: 'PGP',
|
||||||
|
lasUpdate: randomDate(),
|
||||||
|
title: `Some title here ${index}`,
|
||||||
|
text,
|
||||||
|
img: `url("https://picsum.photos/id/${index}/300/200")`,
|
||||||
|
id: String(index),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function randomDate() {
|
||||||
|
return new Date(Date.now() - Math.random() * 1e10)
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { asset } from '$fresh/runtime.ts'
|
import { asset } from '$fresh/runtime.ts'
|
||||||
import AiChatBox from ':islands/AiChatBox.tsx'
|
import SearchBox from '../islands/SearchBox.tsx'
|
||||||
import MoreBox from ':islands/MoreBox.tsx'
|
import ThemePicker from '../islands/ThemePicker.tsx'
|
||||||
import SearchBox from ':islands/SearchBox.tsx'
|
import MoreBox from '../islands/MoreBox.tsx'
|
||||||
import ThemePicker from ':islands/ThemePicker.tsx'
|
import AiChatBox from '../islands/AiChatBox.tsx'
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { SignalLike } from '$fresh/src/types.ts'
|
|
||||||
import { render, RenderOptions } from '@deno/gfm'
|
|
||||||
|
|
||||||
export type MarkdownTheme = 'light' | 'dark' | 'auto'
|
|
||||||
export function Markdown(
|
|
||||||
{ children, theme, options }: {
|
|
||||||
children?: SignalLike<string> | string
|
|
||||||
theme?: SignalLike<MarkdownTheme> | MarkdownTheme
|
|
||||||
options?: RenderOptions
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class='markdown-body'
|
|
||||||
data-color-mode={typeof theme === 'string'
|
|
||||||
? theme
|
|
||||||
: theme?.value ?? 'auto'}
|
|
||||||
data-light-theme='light'
|
|
||||||
data-dark-theme='dark'
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: render(
|
|
||||||
typeof children === 'string' ? children : children?.value ?? '',
|
|
||||||
options,
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import RegisterServiceWorker from ':islands/RegisterServiceWorker.tsx'
|
import RegisterServiceWorker from '../islands/RegisterServiceWorker.tsx'
|
||||||
|
|
||||||
export function ProgressiveWebApp() {
|
export function ProgressiveWebApp() {
|
||||||
return <RegisterServiceWorker />
|
return <RegisterServiceWorker />
|
||||||
|
|
48
deno.json
48
deno.json
|
@ -11,58 +11,34 @@
|
||||||
"serve": "deno task preview",
|
"serve": "deno task preview",
|
||||||
"dev:add_package": "deno run --allow-net=git.cohabit.fr --allow-read=. --allow-write=./deno.json,./packages --allow-run=git,deno ./scripts/add_package.ts"
|
"dev:add_package": "deno run --allow-net=git.cohabit.fr --allow-read=. --allow-write=./deno.json,./packages --allow-run=git,deno ./scripts/add_package.ts"
|
||||||
},
|
},
|
||||||
"fmt": {
|
"fmt": { "singleQuote": true, "semiColons": false, "useTabs": true },
|
||||||
"singleQuote": true,
|
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
|
||||||
"semiColons": false,
|
"exclude": ["**/_fresh/*", "packages/"],
|
||||||
"useTabs": true
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"rules": {
|
|
||||||
"tags": [
|
|
||||||
"fresh",
|
|
||||||
"recommended"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"**/_fresh/*",
|
|
||||||
"packages/"
|
|
||||||
],
|
|
||||||
"imports": {
|
"imports": {
|
||||||
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
||||||
"$std/": "https://deno.land/std@0.208.0/",
|
"$std/": "https://deno.land/std@0.208.0/",
|
||||||
":components/": "./components/",
|
|
||||||
":islands/": "./islands/",
|
|
||||||
":src/": "./src/",
|
|
||||||
"@cohabit/cohamail/": "./packages/@cohabit__cohamail@0.2.1/",
|
"@cohabit/cohamail/": "./packages/@cohabit__cohamail@0.2.1/",
|
||||||
"@cohabit/ressources_manager/": "./packages/@cohabit__ressources_manager@0.1.3/",
|
"@cohabit/ressources_manager/": "./packages/@cohabit__ressources_manager@0.1.2/",
|
||||||
"@deno/gfm": "jsr:@deno/gfm@^0.8.2",
|
|
||||||
"@jotsr/delayed": "jsr:@jotsr/delayed@^2.1.1",
|
"@jotsr/delayed": "jsr:@jotsr/delayed@^2.1.1",
|
||||||
"@jotsr/smart-css-bundler": "jsr:@jotsr/smart-css-bundler@^0.3.0",
|
"@jotsr/smart-css-bundler": "jsr:@jotsr/smart-css-bundler@^0.3.0",
|
||||||
"@preact/signals": "npm:@preact/signals@^1.2.3",
|
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
|
||||||
"@preact/signals-core": "npm:@preact/signals-core@^1.6.1",
|
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
|
||||||
"@simplewebauthn/browser": "npm:@simplewebauthn/browser@^10.0.0",
|
"@simplewebauthn/browser": "npm:@simplewebauthn/browser@^10.0.0",
|
||||||
"@simplewebauthn/server": "npm:@simplewebauthn/server@^10.0.0",
|
"@simplewebauthn/server": "npm:@simplewebauthn/server@^10.0.0",
|
||||||
"@simplewebauthn/types": "npm:@simplewebauthn/types@^10.0.0",
|
"@simplewebauthn/types": "npm:@simplewebauthn/types@^10.0.0",
|
||||||
"@std/encoding": "jsr:@std/encoding@^0.224.3",
|
"@std/encoding": "jsr:@std/encoding@^0.224.3",
|
||||||
"@std/front-matter": "jsr:@std/front-matter@^0.224.2",
|
|
||||||
"@std/http": "jsr:@std/http@^0.224.4",
|
"@std/http": "jsr:@std/http@^0.224.4",
|
||||||
"@std/json": "jsr:@std/json@^0.224.1",
|
|
||||||
"@std/streams": "jsr:@std/streams@^0.224.5",
|
|
||||||
"@univoq/": "https://deno.land/x/univoq@0.2.0/",
|
"@univoq/": "https://deno.land/x/univoq@0.2.0/",
|
||||||
"preact": "npm:preact@^10.22.1",
|
"gfm": "https://deno.land/x/gfm@0.6.0/mod.ts",
|
||||||
|
"preact": "https://esm.sh/preact@10.19.6",
|
||||||
|
"preact/": "https://esm.sh/preact@10.19.6/",
|
||||||
"univoq": "https://deno.land/x/univoq@0.2.0/mod.ts",
|
"univoq": "https://deno.land/x/univoq@0.2.0/mod.ts",
|
||||||
"web-push": "npm:web-push@^3.6.7"
|
"web-push": "npm:web-push@^3.6.7"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
|
||||||
"jsx": "react-jsx",
|
|
||||||
"jsxImportSource": "preact"
|
|
||||||
},
|
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/@cohabit__cohamail@0.2.1",
|
"packages/@cohabit__cohamail@0.2.1",
|
||||||
"packages/@cohabit__ressources_manager@0.1.3"
|
"packages/@cohabit__ressources_manager@0.1.2"
|
||||||
],
|
],
|
||||||
"unstable": [
|
"unstable": ["kv"]
|
||||||
"kv"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { JsonParseStream } from '$std/json/mod.ts'
|
|
||||||
import { CSS, render as renderMd } from '@deno/gfm'
|
|
||||||
import { Signal, signal, useSignal } from '@preact/signals'
|
import { Signal, signal, useSignal } from '@preact/signals'
|
||||||
import { JSX } from 'preact'
|
|
||||||
import { useEffect, useRef } from 'preact/hooks'
|
import { useEffect, useRef } from 'preact/hooks'
|
||||||
|
import { JSX } from 'preact'
|
||||||
|
import { JsonParseStream } from '$std/json/mod.ts'
|
||||||
|
import { CSS, render as renderMd } from 'gfm'
|
||||||
|
|
||||||
const systemHistory = signal<BotMessage[]>([{
|
const systemHistory = signal<BotMessage[]>([{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
import { BlogCard, BlogProps } from ':components/BlogBlocks.tsx'
|
|
||||||
import Suspense from ':islands/Suspens.tsx'
|
|
||||||
import { requestApiStream } from ':src/utils.ts'
|
|
||||||
import { Signal, useSignal } from '@preact/signals'
|
|
||||||
import type { JSX } from 'preact'
|
|
||||||
import { useEffect } from 'preact/hooks'
|
|
||||||
|
|
||||||
function fillList(
|
|
||||||
list: Signal<JSX.Element[]>,
|
|
||||||
{ limit, ac }: { limit?: number; ac?: AbortController },
|
|
||||||
) {
|
|
||||||
;(async () => {
|
|
||||||
const newsList = requestApiStream<void, BlogProps>(
|
|
||||||
'news/fetchAll',
|
|
||||||
'GET',
|
|
||||||
)
|
|
||||||
|
|
||||||
for await (const news of newsList) {
|
|
||||||
list.value = [
|
|
||||||
...list.value,
|
|
||||||
BlogCard({ ...news, lastUpdate: new Date(news.lastUpdate) }),
|
|
||||||
]
|
|
||||||
if (limit && list.value.length >= limit) break
|
|
||||||
}
|
|
||||||
ac?.abort()
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlogCardList(
|
|
||||||
{ limit, usePlaceholder }: { usePlaceholder?: boolean; limit?: number },
|
|
||||||
) {
|
|
||||||
const list = useSignal<JSX.Element[]>([])
|
|
||||||
const ac = new AbortController()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fillList(list, { limit, ac })
|
|
||||||
})
|
|
||||||
|
|
||||||
if (limit && usePlaceholder) {
|
|
||||||
const placeholders = Array
|
|
||||||
.from({ length: limit })
|
|
||||||
.map((_, index) => (
|
|
||||||
<Suspense
|
|
||||||
loader={<Placeholder />}
|
|
||||||
fallback={Fallback}
|
|
||||||
signal={ac.signal}
|
|
||||||
>
|
|
||||||
{updateFromList(list, index)}
|
|
||||||
</Suspense>
|
|
||||||
))
|
|
||||||
return <>{placeholders}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{list}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function Placeholder() {
|
|
||||||
return (
|
|
||||||
<div class='components__blog_block components__blog_block--card components__blog_block--placeholder'>
|
|
||||||
<h3>Chargement ...</h3>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Fallback() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class='components__blog_block components__blog_block--card components__blog_block--fallback'
|
|
||||||
inert
|
|
||||||
>
|
|
||||||
<h3>Pas de news disponible</h3>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFromList(
|
|
||||||
list: Signal<JSX.Element[]>,
|
|
||||||
index: number,
|
|
||||||
): Promise<JSX.Element> {
|
|
||||||
const { promise, resolve } = Promise.withResolvers<JSX.Element>()
|
|
||||||
list.subscribe((value: JSX.Element[]) => {
|
|
||||||
const selected = value.at(index)
|
|
||||||
if (selected) {
|
|
||||||
resolve(selected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return promise
|
|
||||||
}
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { requestApi } from ':src/utils.ts'
|
|
||||||
import { startAuthentication } from '@simplewebauthn/browser'
|
import { startAuthentication } from '@simplewebauthn/browser'
|
||||||
import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'
|
import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'
|
||||||
import { Button, Input } from 'univoq'
|
import { Button, Input } from 'univoq'
|
||||||
|
@ -6,6 +5,7 @@ import type {
|
||||||
WebAuthnLoginFinishPayload,
|
WebAuthnLoginFinishPayload,
|
||||||
WebAuthnLoginStartPayload,
|
WebAuthnLoginStartPayload,
|
||||||
} from '../routes/api/webauthn/login/[step].ts'
|
} from '../routes/api/webauthn/login/[step].ts'
|
||||||
|
import { requestApi } from '../src/utils.ts'
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { requestApi } from ':src/utils.ts'
|
|
||||||
import { startRegistration } from '@simplewebauthn/browser'
|
import { startRegistration } from '@simplewebauthn/browser'
|
||||||
import { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types'
|
import { PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/types'
|
||||||
import { Button, Input } from 'univoq'
|
import { Button, Input } from 'univoq'
|
||||||
|
@ -6,6 +5,7 @@ import type {
|
||||||
WebAuthnRegisterFinishPayload,
|
WebAuthnRegisterFinishPayload,
|
||||||
WebAuthnRegisterStartPayload,
|
WebAuthnRegisterStartPayload,
|
||||||
} from '../routes/api/webauthn/register/[step].ts'
|
} from '../routes/api/webauthn/register/[step].ts'
|
||||||
|
import { requestApi } from '../src/utils.ts'
|
||||||
|
|
||||||
function isWebAuthnSupported(): boolean {
|
function isWebAuthnSupported(): boolean {
|
||||||
return 'credentials' in navigator
|
return 'credentials' in navigator
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { requestApi } from ':src/utils.ts'
|
import { requestApi } from '../src/utils.ts'
|
||||||
|
|
||||||
export default function RegisterServiceWorker() {
|
export default function RegisterServiceWorker() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { main } from ':src/serviceworker/mod.ts'
|
import { main } from '../src/serviceworker/mod.ts'
|
||||||
|
|
||||||
const IS_SW = 'onpushsubscriptionchange' in self
|
const IS_SW = 'onpushsubscriptionchange' in self
|
||||||
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
// import { Suspense } from '@univoq // Error - mismatch in imports version
|
|
||||||
import { JSX } from 'preact'
|
|
||||||
import { useSignal } from '@preact/signals'
|
|
||||||
|
|
||||||
function RenderError(
|
|
||||||
{ error, fallback }: { error: Error; fallback: Fallback | undefined },
|
|
||||||
) {
|
|
||||||
if (fallback) {
|
|
||||||
return fallback({ error })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<output>
|
|
||||||
<pre>{String(error)}</pre>
|
|
||||||
</output>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Fallback = ({ error }: { error: Error }) => JSX.Element
|
|
||||||
|
|
||||||
export default function Suspense(
|
|
||||||
{ loader, fallback, signal, children }: {
|
|
||||||
loader: JSX.Element
|
|
||||||
children: Promise<JSX.Element>
|
|
||||||
fallback?: Fallback
|
|
||||||
signal?: AbortSignal
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const displayed = useSignal(loader)
|
|
||||||
let loaded = false
|
|
||||||
|
|
||||||
signal?.addEventListener('abort', () => {
|
|
||||||
if (loaded) return
|
|
||||||
try {
|
|
||||||
signal.throwIfAborted()
|
|
||||||
} catch (error) {
|
|
||||||
displayed.value = RenderError({ error, fallback })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
children
|
|
||||||
.then((element) => {
|
|
||||||
if (signal?.aborted) return
|
|
||||||
displayed.value = element
|
|
||||||
loaded = true
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (signal?.aborted) return
|
|
||||||
displayed.value = RenderError({ error, fallback })
|
|
||||||
})
|
|
||||||
|
|
||||||
return <>{displayed}</>
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { asset, Head, Partial } from '$fresh/runtime.ts'
|
import { asset, Head, Partial } from '$fresh/runtime.ts'
|
||||||
import { type PageProps } from '$fresh/server.ts'
|
import { type PageProps } from '$fresh/server.ts'
|
||||||
import { Footer } from ':components/Footer.tsx'
|
import { Footer } from '../components/Footer.tsx'
|
||||||
import { Header } from ':components/Header.tsx'
|
import { Header } from '../components/Header.tsx'
|
||||||
import { ProgressiveWebApp } from ':components/ProgressiveWebApp.tsx'
|
import { ProgressiveWebApp } from '../components/ProgressiveWebApp.tsx'
|
||||||
|
|
||||||
export default function App({ Component }: PageProps) {
|
export default function App({ Component }: PageProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -38,7 +38,13 @@ export default function App({ Component }: PageProps) {
|
||||||
type='image/x-icon'
|
type='image/x-icon'
|
||||||
/>
|
/>
|
||||||
<link rel='stylesheet' href={asset('/main.css')} />
|
<link rel='stylesheet' href={asset('/main.css')} />
|
||||||
<link rel='stylesheet' href={asset('/imports/markdown_css')} />
|
{/* TODO remove google fonts link */}
|
||||||
|
<link rel='preconnect' href='https://fonts.googleapis.com' />
|
||||||
|
<link
|
||||||
|
rel='preconnect'
|
||||||
|
href='https://fonts.gstatic.com'
|
||||||
|
crossorigin={''}
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { FreshContext } from '$fresh/server.ts'
|
import { FreshContext } from '$fresh/server.ts'
|
||||||
import { SessionStore } from ':src/session/mod.ts'
|
|
||||||
import { getCookies, setCookie } from '@std/http/cookie'
|
import { getCookies, setCookie } from '@std/http/cookie'
|
||||||
|
import { SessionStore } from '../src/session/mod.ts'
|
||||||
|
|
||||||
export async function handler(request: Request, ctx: FreshContext) {
|
export async function handler(request: Request, ctx: FreshContext) {
|
||||||
// Update fresh context state with session
|
// Update fresh context state with session
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { FreshContext } from '$fresh/server.ts'
|
import { FreshContext } from '$fresh/server.ts'
|
||||||
import { SessionStore } from ':src/session/mod.ts'
|
import { SessionStore } from '../../src/session/mod.ts'
|
||||||
import { respondApi } from ':src/utils.ts'
|
import { respondApi } from '../../src/utils.ts'
|
||||||
|
|
||||||
export function handler(request: Request, ctx: FreshContext) {
|
export function handler(request: Request, ctx: FreshContext) {
|
||||||
// Check CSRF token
|
// Check CSRF token
|
||||||
|
|
|
@ -2,13 +2,13 @@ import 'npm:iterator-polyfill'
|
||||||
// Polyfill AsyncIterator
|
// Polyfill AsyncIterator
|
||||||
|
|
||||||
import { FreshContext } from '$fresh/server.ts'
|
import { FreshContext } from '$fresh/server.ts'
|
||||||
import { db } from ':src/db/mod.ts'
|
|
||||||
import { SessionHandlers, SessionStore } from ':src/session/mod.ts'
|
|
||||||
import { respondApi } from ':src/utils.ts'
|
|
||||||
import { Contact, type Mail, send } from '@cohabit/cohamail/mod.ts'
|
import { Contact, type Mail, send } from '@cohabit/cohamail/mod.ts'
|
||||||
import { magicLinkTemplate } from '@cohabit/cohamail/templates/mod.ts'
|
import { magicLinkTemplate } from '@cohabit/cohamail/templates/mod.ts'
|
||||||
import { User } from '@cohabit/ressources_manager/src/models/mod.ts'
|
import { SessionHandlers, SessionStore } from '../../../src/session/mod.ts'
|
||||||
|
import { respondApi } from '../../../src/utils.ts'
|
||||||
import { sleep } from '@jotsr/delayed'
|
import { sleep } from '@jotsr/delayed'
|
||||||
|
import { User } from '@cohabit/ressources_manager/src/models/mod.ts'
|
||||||
|
import { db } from '../../../src/db/mod.ts'
|
||||||
|
|
||||||
type MagicLinkInfos = {
|
type MagicLinkInfos = {
|
||||||
remoteId: string
|
remoteId: string
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { fetchNewsList } from ':src/blog/mod.ts'
|
|
||||||
import { SessionHandlers } from ':src/session/mod.ts'
|
|
||||||
import { respondApi, respondApiStream } from ':src/utils.ts'
|
|
||||||
|
|
||||||
export const handler: SessionHandlers = {
|
|
||||||
GET() {
|
|
||||||
try {
|
|
||||||
const newsList = fetchNewsList('cohabit')
|
|
||||||
return respondApiStream(newsList)
|
|
||||||
} catch (error) {
|
|
||||||
return respondApi('error', error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { db } from ':src/db/mod.ts'
|
|
||||||
import type { SessionHandlers } from ':src/session/mod.ts'
|
|
||||||
import { respondApi } from ':src/utils.ts'
|
|
||||||
import { getRelyingParty } from ':src/webauthn/mod.ts'
|
|
||||||
import { Credential, Ref, User } from '@cohabit/ressources_manager/mod.ts'
|
|
||||||
import { Passkey } from '@cohabit/ressources_manager/src/models/src/credential.ts'
|
|
||||||
import {
|
import {
|
||||||
generateAuthenticationOptions,
|
generateAuthenticationOptions,
|
||||||
verifyAuthenticationResponse,
|
verifyAuthenticationResponse,
|
||||||
} from '@simplewebauthn/server'
|
} from '@simplewebauthn/server'
|
||||||
|
import { getRelyingParty } from '../../../../src/webauthn/mod.ts'
|
||||||
import {
|
import {
|
||||||
AuthenticationResponseJSON,
|
AuthenticationResponseJSON,
|
||||||
PublicKeyCredentialRequestOptionsJSON,
|
PublicKeyCredentialRequestOptionsJSON,
|
||||||
} from '@simplewebauthn/types'
|
} from '@simplewebauthn/types'
|
||||||
|
import { respondApi } from '../../../../src/utils.ts'
|
||||||
|
import type { SessionHandlers } from '../../../../src/session/mod.ts'
|
||||||
|
import { db } from '../../../../src/db/mod.ts'
|
||||||
|
import { Credential, Ref, User } from '@cohabit/ressources_manager/mod.ts'
|
||||||
|
import { Passkey } from '@cohabit/ressources_manager/src/models/src/credential.ts'
|
||||||
import { decodeBase64 } from '@std/encoding'
|
import { decodeBase64 } from '@std/encoding'
|
||||||
|
|
||||||
type Params = { step: 'start' | 'finish' }
|
type Params = { step: 'start' | 'finish' }
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { SessionHandlers } from ':src/session/mod.ts'
|
|
||||||
import { respondApi } from ':src/utils.ts'
|
|
||||||
import {
|
import {
|
||||||
generateRegistrationOptions,
|
generateRegistrationOptions,
|
||||||
verifyRegistrationResponse,
|
verifyRegistrationResponse,
|
||||||
|
@ -8,13 +6,15 @@ import type {
|
||||||
PublicKeyCredentialCreationOptionsJSON,
|
PublicKeyCredentialCreationOptionsJSON,
|
||||||
RegistrationResponseJSON,
|
RegistrationResponseJSON,
|
||||||
} from '@simplewebauthn/types'
|
} from '@simplewebauthn/types'
|
||||||
|
import { respondApi } from '../../../../src/utils.ts'
|
||||||
|
import { SessionHandlers } from '../../../../src/session/mod.ts'
|
||||||
|
|
||||||
//TODO improve workspace imports
|
//TODO improve workspace imports
|
||||||
import { db } from ':src/db/mod.ts'
|
|
||||||
import { getRelyingParty } from ':src/webauthn/mod.ts'
|
|
||||||
import { Credential, Ref, User } from '@cohabit/ressources_manager/mod.ts'
|
|
||||||
import { Passkey } from '@cohabit/ressources_manager/src/models/src/credential.ts'
|
import { Passkey } from '@cohabit/ressources_manager/src/models/src/credential.ts'
|
||||||
|
import { Credential, Ref, User } from '@cohabit/ressources_manager/mod.ts'
|
||||||
|
import { getRelyingParty } from '../../../../src/webauthn/mod.ts'
|
||||||
import { encodeBase64 } from '@std/encoding'
|
import { encodeBase64 } from '@std/encoding'
|
||||||
|
import { db } from '../../../../src/db/mod.ts'
|
||||||
|
|
||||||
type Params = { step: 'start' | 'finish' }
|
type Params = { step: 'start' | 'finish' }
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Handlers } from '$fresh/server.ts'
|
import { Handlers } from '$fresh/server.ts'
|
||||||
import { respondApi } from ':src/utils.ts'
|
import { respondApi } from '../../../src/utils.ts'
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async POST(request: Request) {
|
async POST(request: Request) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Handlers } from '$fresh/server.ts'
|
import { Handlers } from '$fresh/server.ts'
|
||||||
import { respondApi } from ':src/utils.ts'
|
import { respondApi } from '../../../src/utils.ts'
|
||||||
import { publicKey } from ':src/webpush/mod.ts'
|
import { publicKey } from '../../../src/webpush/mod.ts'
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
GET() {
|
GET() {
|
||||||
|
|
10
routes/blog/[id].tsx
Normal file
10
routes/blog/[id].tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { PageProps } from '$fresh/server.ts'
|
||||||
|
import { BlogCard, blogMock } from '../../components/BlogCard.tsx'
|
||||||
|
|
||||||
|
export default function Projet({ params }: PageProps) {
|
||||||
|
const article = blogMock.at(Number(params.id))
|
||||||
|
|
||||||
|
return (
|
||||||
|
article ? BlogCard(article) : <h3>Article inconnu</h3>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
import { RouteContext } from '$fresh/server.ts'
|
|
||||||
import { BlogPost } from ':components/BlogBlocks.tsx'
|
|
||||||
import { fetchNews } from ':src/blog/mod.ts'
|
|
||||||
|
|
||||||
export default async function Blog(_req: Request, { params }: RouteContext) {
|
|
||||||
try {
|
|
||||||
const article = await fetchNews('cohabit', params.name)
|
|
||||||
return BlogPost(article)
|
|
||||||
} catch {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>Une erreur est survenue</h3>
|
|
||||||
<p>{`Impossible de récupérer l'article "${params.name}"`}.</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { AutoGrid } from ':components/AutoGrid.tsx'
|
import { AutoGrid } from '../../components/AutoGrid.tsx'
|
||||||
import BlogCardList from ':islands/BlogCardList.tsx'
|
import { BlogCard, blogMock } from '../../components/BlogCard.tsx'
|
||||||
|
|
||||||
export default function Blog() {
|
export default function Blog() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Nos articles</h1>
|
<h1>Nos articles</h1>
|
||||||
<AutoGrid columnWidth='15rem'>
|
<AutoGrid columnWidth='15rem'>
|
||||||
<BlogCardList />
|
{blogMock.map(BlogCard)}
|
||||||
</AutoGrid>
|
</AutoGrid>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Handlers } from '$fresh/server.ts'
|
|
||||||
import { CSS, KATEX_CSS } from '@deno/gfm'
|
|
||||||
|
|
||||||
export const handler: Handlers = {
|
|
||||||
GET() {
|
|
||||||
const styles = CSS + KATEX_CSS
|
|
||||||
return new Response(styles, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/css; charset=utf-8',
|
|
||||||
// TODO add cache headers and eTag
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Head } from '$fresh/runtime.ts'
|
import { Head } from '$fresh/runtime.ts'
|
||||||
import { AutoGrid } from ':components/AutoGrid.tsx'
|
import { AutoGrid } from '../components/AutoGrid.tsx'
|
||||||
import { CohabitInfoTable } from ':components/CohabitInfoTable.tsx'
|
import { BlogCard, blogMock } from '../components/BlogCard.tsx'
|
||||||
import { Heros } from ':components/Heros.tsx'
|
import { CohabitInfoTable } from '../components/CohabitInfoTable.tsx'
|
||||||
import { MachineCard, machineMock } from ':components/MachineCard.tsx'
|
import { Heros } from '../components/Heros.tsx'
|
||||||
import { MemberCard, memberMock } from ':components/MemberCard.tsx'
|
import { MachineCard, machineMock } from '../components/MachineCard.tsx'
|
||||||
import { ProjectCard, projectMock } from ':components/ProjectCard.tsx'
|
import { MemberCard, memberMock } from '../components/MemberCard.tsx'
|
||||||
import { SponsorCards } from ':components/SponsorCards.tsx'
|
import { ProjectCard, projectMock } from '../components/ProjectCard.tsx'
|
||||||
import BlogCardList from ':islands/BlogCardList.tsx'
|
import { SponsorCards } from '../components/SponsorCards.tsx'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
|
@ -17,9 +17,9 @@ export default function Home() {
|
||||||
<Heros />
|
<Heros />
|
||||||
<section id='first-section'>
|
<section id='first-section'>
|
||||||
<h2>Nos actus</h2>
|
<h2>Nos actus</h2>
|
||||||
<AutoGrid columnWidth='15rem' style={{ alignItems: 'center' }}>
|
<AutoGrid columnWidth='15rem'>
|
||||||
<>
|
<>
|
||||||
<BlogCardList limit={4} usePlaceholder={true} />
|
{blogMock.slice(0, 4).map(BlogCard)}
|
||||||
<a href='/blog' class='cta'>Voir plus</a>
|
<a href='/blog' class='cta'>Voir plus</a>
|
||||||
</>
|
</>
|
||||||
</AutoGrid>
|
</AutoGrid>
|
||||||
|
@ -27,11 +27,10 @@ export default function Home() {
|
||||||
<section>
|
<section>
|
||||||
<h2>Nos machines</h2>
|
<h2>Nos machines</h2>
|
||||||
<p>
|
<p>
|
||||||
Vous avez besoin d'aide pour concrétiser votre projet ? Le
|
Vous avez besoin d'aide pour concrétiser votre projet ? Le Fablab vous
|
||||||
Fablab vous accompagnes dans vos projets, grâce à son parc
|
accompagnes dans vos projets, grâce à son parc de machine...
|
||||||
de machine...
|
|
||||||
</p>
|
</p>
|
||||||
<AutoGrid columnWidth='15rem' style={{ alignItems: 'center' }}>
|
<AutoGrid columnWidth='15rem'>
|
||||||
<>
|
<>
|
||||||
{machineMock.slice(0, 4).map(MachineCard)}
|
{machineMock.slice(0, 4).map(MachineCard)}
|
||||||
<a href='/machines' class='cta'>Réserver</a>
|
<a href='/machines' class='cta'>Réserver</a>
|
||||||
|
@ -40,7 +39,7 @@ export default function Home() {
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>Nos projets</h2>
|
<h2>Nos projets</h2>
|
||||||
<AutoGrid columnWidth='30rem' style={{ alignItems: 'center' }}>
|
<AutoGrid columnWidth='30rem'>
|
||||||
<>
|
<>
|
||||||
{projectMock.slice(0, 4).map(ProjectCard)}
|
{projectMock.slice(0, 4).map(ProjectCard)}
|
||||||
<a href='/projets' class='cta'>Participer</a>
|
<a href='/projets' class='cta'>Participer</a>
|
||||||
|
@ -49,7 +48,7 @@ export default function Home() {
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>Nos membres</h2>
|
<h2>Nos membres</h2>
|
||||||
<AutoGrid columnWidth='15rem' style={{ alignItems: 'center' }}>
|
<AutoGrid columnWidth='15rem'>
|
||||||
<>
|
<>
|
||||||
{memberMock.slice(0, 4).map(MemberCard)}
|
{memberMock.slice(0, 4).map(MemberCard)}
|
||||||
<a href='/membres' class='cta'>Nous découvrir</a>
|
<a href='/membres' class='cta'>Nous découvrir</a>
|
||||||
|
@ -59,29 +58,26 @@ export default function Home() {
|
||||||
<section>
|
<section>
|
||||||
<h2>Présentation</h2>
|
<h2>Présentation</h2>
|
||||||
<p>
|
<p>
|
||||||
Coh@bit est un fablab de l'université de Bordeaux ouvert à
|
Coh@bit est un fablab de l'université de Bordeaux ouvert à tous les
|
||||||
tous les publics depuis 2016. Du collégien à
|
publics depuis 2016. Du collégien à l'enseignant-chercheur, l'équipe
|
||||||
l'enseignant-chercheur, l'équipe du fablab accompagne les
|
du fablab accompagne les adhérents dans la réalisation de leurs
|
||||||
adhérents dans la réalisation de leurs projets de
|
projets de fabrication autour du numérique.
|
||||||
fabrication autour du numérique.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Venez découvrir un tout nouvelle univers où vous pouvez
|
Venez découvrir un tout nouvelle univers où vous pouvez concrétiser
|
||||||
concrétiser vos projet, découvrir des personnes avec les
|
vos projet, découvrir des personnes avec les même affinités que vous,
|
||||||
même affinités que vous, cultiver votre savoir et savoir
|
cultiver votre savoir et savoir faire, dans l'entraide et le partage.
|
||||||
faire, dans l'entraide et le partage.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Créer par Frédéric Bos (Directeur de l'IUT de Bordeaux) en
|
Créer par Frédéric Bos (Directeur de l'IUT de Bordeaux) en 2014,
|
||||||
2014, Coh@bit (Creative Open House at Bordeaux Institut of
|
Coh@bit (Creative Open House at Bordeaux Institut of Technology) est
|
||||||
Technology) est une association réunissant deux entités : le
|
une association réunissant deux entités : le Fablab et le Technoshop.
|
||||||
Fablab et le Technoshop.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Ouvert à tous les publics depuis 2016, allant de
|
Ouvert à tous les publics depuis 2016, allant de
|
||||||
l'enseignant-chercheur au collégien, l'équipe du fablab
|
l'enseignant-chercheur au collégien, l'équipe du fablab accompagne les
|
||||||
accompagne les adhérents dans la réalisation de leurs
|
adhérents dans la réalisation de leurs projets de fabrication
|
||||||
projets de fabrication numérique.
|
numérique.
|
||||||
</p>
|
</p>
|
||||||
<CohabitInfoTable />
|
<CohabitInfoTable />
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PageProps } from '$fresh/server.ts'
|
import { PageProps } from '$fresh/server.ts'
|
||||||
import { MachineCard, machineMock } from ':components/MachineCard.tsx'
|
import { MachineCard, machineMock } from '../../components/MachineCard.tsx'
|
||||||
|
|
||||||
export default function Machine({ params }: PageProps) {
|
export default function Machine({ params }: PageProps) {
|
||||||
const machine = machineMock.at(Number(params.id))
|
const machine = machineMock.at(Number(params.id))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { AutoGrid } from ':components/AutoGrid.tsx'
|
import { AutoGrid } from '../../components/AutoGrid.tsx'
|
||||||
import { MachineCard, machineMock } from ':components/MachineCard.tsx'
|
import { MachineCard, machineMock } from '../../components/MachineCard.tsx'
|
||||||
|
|
||||||
export default function Machine() {
|
export default function Machine() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,6 +1,19 @@
|
||||||
import { PageProps } from '$fresh/server.ts'
|
import { PageProps } from '$fresh/server.ts'
|
||||||
import { Markdown } from ':components/Markdown.tsx'
|
import { MemberCard, memberMock } from '../../../components/MemberCard.tsx'
|
||||||
import { MemberCard, memberMock } from ':components/MemberCard.tsx'
|
import { CSS, render as renderMd } from 'gfm'
|
||||||
|
|
||||||
|
function Markdown({ children }: { children: string }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style dangerouslySetInnerHTML={{ __html: CSS }}></style>
|
||||||
|
<div
|
||||||
|
class='markdown-body'
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderMd(children) }}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const db = [
|
const db = [
|
||||||
'julien.oculi',
|
'julien.oculi',
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { AutoGrid } from ':components/AutoGrid.tsx'
|
import { AutoGrid } from '../../components/AutoGrid.tsx'
|
||||||
import { MemberCard, memberMock } from ':components/MemberCard.tsx'
|
import { MemberCard, memberMock } from '../../components/MemberCard.tsx'
|
||||||
|
|
||||||
export default function Membres() {
|
export default function Membres() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import LoginForm from ':islands/LoginForm.tsx'
|
|
||||||
import PassKeyRegister from ':islands/PassKeyRegister.tsx'
|
|
||||||
import type { SessionPageProps } from ':src/session/mod.ts'
|
|
||||||
import type { User } from '@cohabit/ressources_manager/mod.ts'
|
|
||||||
import { Button } from 'univoq'
|
import { Button } from 'univoq'
|
||||||
|
import LoginForm from '../../islands/LoginForm.tsx'
|
||||||
|
import PassKeyRegister from '../../islands/PassKeyRegister.tsx'
|
||||||
|
import type { SessionPageProps } from '../../src/session/mod.ts'
|
||||||
|
import type { User } from '@cohabit/ressources_manager/mod.ts'
|
||||||
|
|
||||||
export default function Profil({ state }: SessionPageProps) {
|
export default function Profil({ state }: SessionPageProps) {
|
||||||
const user = state.session?.get<User>('user')
|
const user = state.session?.get<User>('user')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PageProps } from '$fresh/server.ts'
|
import { PageProps } from '$fresh/server.ts'
|
||||||
import { ProjectCard, projectMock } from ':components/ProjectCard.tsx'
|
import { ProjectCard, projectMock } from '../../components/ProjectCard.tsx'
|
||||||
|
|
||||||
export default function Projets({ params }: PageProps) {
|
export default function Projets({ params }: PageProps) {
|
||||||
const Projets = projectMock.at(Number(params.id))
|
const Projets = projectMock.at(Number(params.id))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { AutoGrid } from ':components/AutoGrid.tsx'
|
import { AutoGrid } from '../../components/AutoGrid.tsx'
|
||||||
import { ProjectCard, projectMock } from ':components/ProjectCard.tsx'
|
import { ProjectCard, projectMock } from '../../components/ProjectCard.tsx'
|
||||||
|
|
||||||
export default function Project() {
|
export default function Project() {
|
||||||
return (
|
return (
|
||||||
|
|
104
src/blog/mod.ts
104
src/blog/mod.ts
|
@ -1,104 +0,0 @@
|
||||||
import { BlogProps } from ':components/BlogBlocks.tsx'
|
|
||||||
import { NewsFrontMatter } from ':src/blog/types.ts'
|
|
||||||
import { base64ToString } from ':src/utils.ts'
|
|
||||||
import { extract } from '@std/front-matter/yaml'
|
|
||||||
|
|
||||||
export async function fetchNews(
|
|
||||||
publisher: string,
|
|
||||||
name: string,
|
|
||||||
): Promise<BlogProps> {
|
|
||||||
const apiUrl = 'https://git.cohabit.fr/api/v1/'
|
|
||||||
const baseEndpoint = new URL(`repos/${publisher}/.news/`, apiUrl)
|
|
||||||
const endpoint = new URL('contents/', baseEndpoint)
|
|
||||||
|
|
||||||
// Get readme content api url
|
|
||||||
const readmePath = encodeURIComponent(`${name}/README.md`)
|
|
||||||
const contentUrl = new URL(readmePath, endpoint)
|
|
||||||
|
|
||||||
// Fetch readme content, commit hash and raw url for relative links
|
|
||||||
const file = await getCommitAndContent(contentUrl)
|
|
||||||
// Get commit infos (author + date) and get readme content from base64 source
|
|
||||||
const { raw, url, lastUpdate, author } = await getAuthorAndParseContent(
|
|
||||||
file,
|
|
||||||
baseEndpoint,
|
|
||||||
)
|
|
||||||
// Extract frontmatter
|
|
||||||
const { attrs, body } = extract<NewsFrontMatter>(raw)
|
|
||||||
|
|
||||||
// Transform API responses into BlogProps for BlogCard and BlogPost components
|
|
||||||
return {
|
|
||||||
author,
|
|
||||||
publisher,
|
|
||||||
lastUpdate,
|
|
||||||
options: attrs['x-cohabit'],
|
|
||||||
title: attrs.title,
|
|
||||||
hash: file.sha,
|
|
||||||
description: attrs.description,
|
|
||||||
body,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
tags: attrs.tags,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function* fetchNewsList(
|
|
||||||
publisher: string,
|
|
||||||
): AsyncGenerator<BlogProps, void, void> {
|
|
||||||
const apiUrl = 'https://git.cohabit.fr/api/v1/'
|
|
||||||
const baseEndpoint = new URL(`repos/${publisher}/.news/`, apiUrl)
|
|
||||||
const endpoint = new URL('contents/', baseEndpoint)
|
|
||||||
|
|
||||||
// Fetch repo content
|
|
||||||
const root = await fetch(endpoint).then((response) => response.json()) as {
|
|
||||||
name: string
|
|
||||||
type: string
|
|
||||||
}[]
|
|
||||||
|
|
||||||
// Fetch `README.md` in sub directories
|
|
||||||
const blogPropsList = root
|
|
||||||
// Remove file and dir starting with "."
|
|
||||||
.filter(isNewsDirectory)
|
|
||||||
// Fetch single news and return BlogProps
|
|
||||||
.map(({ name }) => fetchNews(publisher, name))
|
|
||||||
|
|
||||||
// Yield each news
|
|
||||||
for (const blogProps of blogPropsList) {
|
|
||||||
yield blogProps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAuthorAndParseContent(
|
|
||||||
file: { download_url: string; content: string; last_commit_sha: string },
|
|
||||||
baseEndpoint: URL,
|
|
||||||
) {
|
|
||||||
const commitUrl = new URL(
|
|
||||||
`git/commits/${file.last_commit_sha}?stat=false&verification=false&files=false`,
|
|
||||||
baseEndpoint,
|
|
||||||
)
|
|
||||||
const infos = await fetch(commitUrl).then((response) =>
|
|
||||||
response.json()
|
|
||||||
) as {
|
|
||||||
created: string
|
|
||||||
author: { login: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
raw: base64ToString(file.content),
|
|
||||||
url: file.download_url,
|
|
||||||
lastUpdate: new Date(infos.created),
|
|
||||||
author: infos.author.login,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCommitAndContent(contentUrl: URL) {
|
|
||||||
return await fetch(contentUrl).then((response) => response.json()) as {
|
|
||||||
download_url: string
|
|
||||||
content: string
|
|
||||||
sha: string
|
|
||||||
last_commit_sha: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNewsDirectory(entry: { name: string; type: string }): boolean {
|
|
||||||
return entry.type === 'dir' && entry.name.startsWith('.') === false
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
export type NewsFrontMatter = {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
tags?: string[]
|
|
||||||
'x-cohabit': {
|
|
||||||
links?: Record<string, string>[]
|
|
||||||
status: 'canceled' | 'futur' | 'current' | 'finished'
|
|
||||||
visibility?: 'public' | 'internal'
|
|
||||||
thumbnail: string
|
|
||||||
dueDate: string
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -42,7 +42,6 @@
|
||||||
--_gap-half: calc(var(--_gap) / 2);
|
--_gap-half: calc(var(--_gap) / 2);
|
||||||
--_wide-screen: 1400px;
|
--_wide-screen: 1400px;
|
||||||
--_small-screen: 800px;
|
--_small-screen: 800px;
|
||||||
--_readable-screen: max(80dvw, var(--_small-screen));
|
|
||||||
|
|
||||||
/* color */
|
/* color */
|
||||||
--_accent-color: var(--lime-6);
|
--_accent-color: var(--lime-6);
|
||||||
|
@ -147,7 +146,6 @@ input[type='checkbox'] {
|
||||||
transition: all var(--_transition-delay) ease;
|
transition: all var(--_transition-delay) ease;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta:hover,
|
.cta:hover,
|
||||||
|
@ -168,7 +166,3 @@ input[type='checkbox'] {
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
padding: var(--_gap);
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
@import url('../../components/Heros.css');
|
@import url('../../components/Heros.css');
|
||||||
@import url('../../components/SponsorCards.css');
|
@import url('../../components/SponsorCards.css');
|
||||||
@import url('../../components/CohabitInfoTable.css');
|
@import url('../../components/CohabitInfoTable.css');
|
||||||
@import url('../../components/BlogBlocks.css');
|
@import url('../../components/BlogCard.css');
|
||||||
@import url('../../components/MachineCard.css');
|
@import url('../../components/MachineCard.css');
|
||||||
@import url('../../components/ProjectCard.css');
|
@import url('../../components/ProjectCard.css');
|
||||||
@import url('../../components/AutoGrid.css');
|
@import url('../../components/AutoGrid.css');
|
||||||
|
|
94
src/utils.ts
94
src/utils.ts
|
@ -1,8 +1,4 @@
|
||||||
import { JsonValue } from '$std/json/common.ts'
|
import { JsonValue } from '$std/json/common.ts'
|
||||||
import { decodeBase64 } from '@std/encoding/base64'
|
|
||||||
import { JsonStringifyStream } from '@std/json'
|
|
||||||
import { JsonParseStream } from '@std/json/json-parse-stream'
|
|
||||||
import { TextLineStream } from '@std/streams/text-line-stream'
|
|
||||||
|
|
||||||
export type JsonCompatible = JsonValue | { toJSON(): JsonValue } | unknown
|
export type JsonCompatible = JsonValue | { toJSON(): JsonValue } | unknown
|
||||||
|
|
||||||
|
@ -74,91 +70,6 @@ export type ApiPayload<ApiResponse extends JsonCompatible = never> = {
|
||||||
error: string
|
error: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function respondApiStream<
|
|
||||||
Payload extends JsonCompatible,
|
|
||||||
>(
|
|
||||||
source:
|
|
||||||
| ReadableStream<Payload>
|
|
||||||
| Iterable<Payload>
|
|
||||||
| AsyncIterable<Payload>,
|
|
||||||
): Promise<Response> {
|
|
||||||
const stream = new TransformStream<
|
|
||||||
ApiPayload<Payload>,
|
|
||||||
ApiPayload<Payload>
|
|
||||||
>()
|
|
||||||
const writer = stream.writable.getWriter()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await writer.ready
|
|
||||||
|
|
||||||
for await (const data of source) {
|
|
||||||
writer.write({ kind: 'success', data })
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
writer.write({ kind: 'error', error })
|
|
||||||
} finally {
|
|
||||||
writer.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = stream.readable
|
|
||||||
.pipeThrough(new JsonStringifyStream())
|
|
||||||
.pipeThrough(new TextEncoderStream())
|
|
||||||
|
|
||||||
return new Response(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function* requestApiStream<
|
|
||||||
Payload extends JsonCompatible | undefined,
|
|
||||||
ApiResponse extends JsonCompatible,
|
|
||||||
>(
|
|
||||||
route: string,
|
|
||||||
method: 'GET' | 'POST' | 'DELETE' | 'PATCH',
|
|
||||||
payload?: Payload | null,
|
|
||||||
): AsyncGenerator<ApiResponse, void, void> {
|
|
||||||
const csrf = getCookie('_CSRF') ?? ''
|
|
||||||
|
|
||||||
const base = new URL('/api/', location.origin)
|
|
||||||
const endpoint = new URL(
|
|
||||||
route.startsWith('/') ? `.${route}` : route,
|
|
||||||
base.href,
|
|
||||||
)
|
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
|
||||||
'X-CSRF-TOKEN': csrf,
|
|
||||||
},
|
|
||||||
body: payload ? JSON.stringify(payload) : null,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { body } = response
|
|
||||||
|
|
||||||
if (body === null) {
|
|
||||||
throw new TypeError(`api response stream is null`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = body
|
|
||||||
.pipeThrough(new TextDecoderStream()) // convert Uint8Array to string
|
|
||||||
.pipeThrough(new TextLineStream()) // transform into a stream where each chunk is divided by a newline
|
|
||||||
.pipeThrough(new JsonParseStream()) as unknown as ReadableStream<
|
|
||||||
ApiPayload<ApiResponse>
|
|
||||||
> // parse each chunk as JSON
|
|
||||||
|
|
||||||
for await (const payload of stream) {
|
|
||||||
if (payload.kind === 'error') {
|
|
||||||
throw new Error(
|
|
||||||
`api stream error while getting "${endpoint.href}"`,
|
|
||||||
{
|
|
||||||
cause: payload.error,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
yield payload.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookie(name: string): string | undefined {
|
function getCookie(name: string): string | undefined {
|
||||||
const cookiesEntries = document.cookie.split(';').map((cookie) =>
|
const cookiesEntries = document.cookie.split(';').map((cookie) =>
|
||||||
cookie.trim().split('=')
|
cookie.trim().split('=')
|
||||||
|
@ -166,8 +77,3 @@ function getCookie(name: string): string | undefined {
|
||||||
const cookies = Object.fromEntries(cookiesEntries)
|
const cookies = Object.fromEntries(cookiesEntries)
|
||||||
return cookies[name]
|
return cookies[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function base64ToString(base64: string): string {
|
|
||||||
const bytes = decodeBase64(base64)
|
|
||||||
return new TextDecoder().decode(bytes)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue