feat: Upgrade template based on projects made with it
Some checks failed
Build, check & Test / run (push) Failing after 42s
Signed-off-by: Florian Bouillon <f.bouillon@aptatio.com>
4
.vscode/extensions.json
vendored
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["astro-build.astro-vscode"],
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
"unwantedRecommendations": []
|
"unwantedRecommendations": []
|
||||||
}
|
}
|
||||||
|
18
.vscode/launch.json
vendored
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"command": "./node_modules/.bin/astro dev",
|
"command": "./node_modules/.bin/astro dev",
|
||||||
"name": "Development server",
|
"name": "Development server",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal"
|
"type": "node-terminal"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# e2e
|
# e2e
|
||||||
|
|
||||||
Hold End 2 End tests
|
Hold End 2 End tests
|
||||||
|
|
||||||
currently WIP
|
|
||||||
|
@ -4,5 +4,5 @@ test('has title', async ({ page }) => {
|
|||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
// Expect a title "to contain" a substring.
|
// Expect a title "to contain" a substring.
|
||||||
await expect(page).toHaveTitle(/Astro/);
|
await expect(page).toHaveTitle(/Dzeio/);
|
||||||
})
|
})
|
||||||
|
@ -22,7 +22,8 @@
|
|||||||
"@dzeio/url-manager": "^1",
|
"@dzeio/url-manager": "^1",
|
||||||
"astro": "^3",
|
"astro": "^3",
|
||||||
"lucide-astro": "^0",
|
"lucide-astro": "^0",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0",
|
||||||
|
"simple-icons-astro": "^9",
|
||||||
"tailwindcss": "^3"
|
"tailwindcss": "^3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -30,7 +31,7 @@
|
|||||||
"@playwright/test": "^1",
|
"@playwright/test": "^1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@vitest/coverage-v8": "^0",
|
"@vitest/coverage-v8": "^0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5",
|
||||||
"vitest": "^0"
|
"vitest": "^0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
src/assets/components/layouts/Header/Logo.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 749 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 758 B After Width: | Height: | Size: 749 B |
1
src/assets/pages/404/404.light.svg
Normal file
After Width: | Height: | Size: 23 KiB |
1
src/assets/pages/404/404.svg
Normal file
After Width: | Height: | Size: 23 KiB |
28
src/components/global/Breadcrumb.astro
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
items: Array<{
|
||||||
|
text: string
|
||||||
|
href?: string | undefined
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ol vocab="https://schema.org/" typeof="BreadcrumbList" class="inline-flex items-center flex-wrap px-0 mb-4">
|
||||||
|
{Astro.props.items.map((el, index) => (
|
||||||
|
<li property="itemListElement" typeof="ListItem" class="inline-block px-0">
|
||||||
|
{index > 0 && (
|
||||||
|
<span class="text-gray-900 dark:text-gray-100 mx-4">/</span>
|
||||||
|
)}
|
||||||
|
{el.href ? (
|
||||||
|
<a class="text-gray-900 dark:text-gray-100 font-normal" href={el.href} property="item" typeof="WebPage">
|
||||||
|
<span property="name">{el.text}</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span class="font-bold" property="name">{el.text}</span>
|
||||||
|
)}
|
||||||
|
<meta property="position" content={index.toString()} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
35
src/components/global/Button.astro
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
|
|
||||||
|
interface Props extends Omit<astroHTML.JSX.ButtonHTMLAttributes | astroHTML.JSX.AnchorHTMLAttributes, 'type'> {
|
||||||
|
type?: 'outline' | 'ghost'
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = [
|
||||||
|
"button",
|
||||||
|
Astro.props.type,
|
||||||
|
Astro.props.class
|
||||||
|
]
|
||||||
|
|
||||||
|
---
|
||||||
|
{'href' in Astro.props && (
|
||||||
|
<a class:list={classes} {...objectOmit(Astro.props, 'type') as any}>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
) || (
|
||||||
|
<button class:list={classes} {...objectOmit(Astro.props, 'type') as any}>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.button {
|
||||||
|
@apply outline-none inline-flex px-4 py-2 rounded-lg bg-amber-500 hover:bg-amber-600 active:bg-amber-700 text-white font-medium transition-colors
|
||||||
|
}
|
||||||
|
.button.outline {
|
||||||
|
@apply bg-transparent border-2 text-amber-500 border-gray-200 hover:bg-gray-100 active:bg-gray-200 active:border-gray-300
|
||||||
|
}
|
||||||
|
.button.ghost {
|
||||||
|
@apply text-black bg-transparent hover:bg-gray-200 active:bg-gray-300
|
||||||
|
}
|
||||||
|
</style>
|
83
src/components/global/Input.astro
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
|
interface Props extends Omit<astroHTML.JSX.InputHTMLAttributes, 'type'> {
|
||||||
|
label?: string
|
||||||
|
type?: astroHTML.JSX.InputHTMLAttributes['type'] | 'textarea'
|
||||||
|
block?: boolean
|
||||||
|
suffix?: string
|
||||||
|
prefix?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix')
|
||||||
|
|
||||||
|
if (baseProps.type === 'textarea') {
|
||||||
|
delete baseProps.type
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- input wrapper -->
|
||||||
|
<label class:list={['parent', {'w-full': Astro.props.block}]}>
|
||||||
|
{Astro.props.label && (
|
||||||
|
<div class="label">{Astro.props.label}</div>
|
||||||
|
)}
|
||||||
|
<!-- input in itself -->
|
||||||
|
<div class="relative input">
|
||||||
|
{Astro.props.prefix && (
|
||||||
|
<p class="prefix">{Astro.props.prefix}</p>
|
||||||
|
)}
|
||||||
|
{Astro.props.type === 'textarea' && (
|
||||||
|
<textarea class="textarea transition-[min-height]" {...baseProps} />
|
||||||
|
) || (
|
||||||
|
<input {...baseProps as any} />
|
||||||
|
)}
|
||||||
|
{Astro.props.suffix && (
|
||||||
|
<p class="suffix">{Astro.props.suffix}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
@apply flex flex-col cursor-text gap-2
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent input, .parent textarea {
|
||||||
|
@apply w-full
|
||||||
|
}
|
||||||
|
|
||||||
|
.suffix, .prefix {
|
||||||
|
@apply select-none font-light text-gray-400
|
||||||
|
}
|
||||||
|
.input, .textarea {
|
||||||
|
@apply px-4 w-full bg-gray-100 rounded-lg border-gray-200 min-h-0 border flex items-center gap-2 py-2
|
||||||
|
}
|
||||||
|
.input textarea, .input input {
|
||||||
|
@apply bg-transparent outline-none invalid:border-red-300 placeholder:text-gray-400 placeholder:font-light focus-visible:outline-none
|
||||||
|
}
|
||||||
|
.textarea {
|
||||||
|
@apply overflow-y-hidden
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Component from 'libs/Component'
|
||||||
|
|
||||||
|
function updateHeight(it: HTMLTextAreaElement) {
|
||||||
|
if (!it.style.height) {
|
||||||
|
it.classList.remove('transition-[min-height]')
|
||||||
|
const previous = it.style.minHeight
|
||||||
|
it.style.minHeight = ''
|
||||||
|
const scrollHeight = it.scrollHeight
|
||||||
|
it.style.minHeight = previous
|
||||||
|
setTimeout(() => {
|
||||||
|
it.style.minHeight = `${scrollHeight}px`
|
||||||
|
it.classList.add('transition-[min-height]')
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component.handle<HTMLTextAreaElement>('textarea', (it) => {
|
||||||
|
updateHeight(it)
|
||||||
|
it.addEventListener('input', () => updateHeight(it))
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { getImage } from 'astro:assets'
|
import { getImage } from 'astro:assets'
|
||||||
import AstroUtils from '../libs/AstroUtils'
|
import AstroUtils from '../../libs/AstroUtils'
|
||||||
import { objectOmit } from '@dzeio/object-util'
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
|
|
||||||
const formats = [
|
const formats = [
|
30
src/components/global/Range.astro
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
|
interface Props extends Omit<astroHTML.JSX.InputHTMLAttributes, 'type'> {
|
||||||
|
label?: string
|
||||||
|
block?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseProps = objectOmit(Astro.props, 'label', 'block')
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={[{parent: Astro.props.block}]}>
|
||||||
|
{Astro.props.label && (
|
||||||
|
<label for={Astro.props.name}>{Astro.props.label}</label>
|
||||||
|
)}
|
||||||
|
<input type="range" class="input" {...baseProps as any} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
@apply w-full
|
||||||
|
}
|
||||||
|
input[type='range'] {
|
||||||
|
@apply appearance-none bg-gray-200 rounded-full h-1 w-full
|
||||||
|
}
|
||||||
|
input[type='range']::-webkit-slider-thumb,
|
||||||
|
input[type='range']::-moz-range-thumb {
|
||||||
|
@apply appearance-none bg-amber-600 w-4 h-4 border-0
|
||||||
|
}
|
||||||
|
</style>
|
103
src/components/global/Select.astro
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
|
|
||||||
|
export interface Props extends Omit<astroHTML.JSX.InputHTMLAttributes, 'type'> {
|
||||||
|
placeholder?: string
|
||||||
|
label?: string
|
||||||
|
block?: boolean
|
||||||
|
suffix?: string
|
||||||
|
prefix?: string
|
||||||
|
options: Array<string | number | {title: string | number, description?: string | number | null}>
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseProps = objectOmit(Astro.props, 'label', 'block', 'suffix', 'prefix', 'options')
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- input wrapper -->
|
||||||
|
<label class:list={['parent', 'select', {'w-full': Astro.props.block}]}>
|
||||||
|
{Astro.props.label && (
|
||||||
|
<div class="label">{Astro.props.label}</div>
|
||||||
|
)}
|
||||||
|
<!-- input in itself -->
|
||||||
|
<div class="relative input">
|
||||||
|
{Astro.props.prefix && (
|
||||||
|
<p class="prefix">{Astro.props.prefix}</p>
|
||||||
|
)}
|
||||||
|
<input readonly {...baseProps as any} />
|
||||||
|
<ul class="list hidden">
|
||||||
|
{Astro.props.options.map((it) => {
|
||||||
|
if (typeof it !== 'object') {
|
||||||
|
it = {title: it}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li data-value={it.title}>
|
||||||
|
<p>{it.title}</p>
|
||||||
|
{it.description && (
|
||||||
|
<p class="desc">{it.description}</p>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
{Astro.props.suffix && (
|
||||||
|
<p class="suffix">{Astro.props.suffix}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
@apply flex flex-col cursor-text gap-2
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent input, .parent textarea {
|
||||||
|
@apply w-full
|
||||||
|
}
|
||||||
|
|
||||||
|
.suffix, .prefix {
|
||||||
|
@apply select-none font-light text-gray-400
|
||||||
|
}
|
||||||
|
.input, .textarea {
|
||||||
|
@apply px-4 w-full bg-gray-100 rounded-lg border-gray-200 min-h-0 border flex items-center gap-2 py-2
|
||||||
|
}
|
||||||
|
.input textarea, .input input {
|
||||||
|
@apply bg-transparent outline-none invalid:border-red-300 placeholder:text-gray-400 placeholder:font-light focus-visible:outline-none
|
||||||
|
}
|
||||||
|
.textarea {
|
||||||
|
@apply overflow-y-hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
@apply absolute top-full mt-2 z-10 bg-gray-50 rounded-lg border-1 border-gray-300 overflow-hidden
|
||||||
|
}
|
||||||
|
input:focus + ul {
|
||||||
|
@apply block
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
@apply px-4 py-2 flex flex-col gap-1 hover:bg-gray-100 cursor-pointer
|
||||||
|
}
|
||||||
|
li p {
|
||||||
|
@apply text-gray-600
|
||||||
|
}
|
||||||
|
li p.desc {
|
||||||
|
@apply text-sm font-light
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Component from 'libs/Component'
|
||||||
|
|
||||||
|
Component.handle<HTMLElement>('.select', (it) => {
|
||||||
|
const input = it.querySelector('input')
|
||||||
|
const list = it.querySelector('ul')
|
||||||
|
if (!input || !list) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list.querySelectorAll('li').forEach((listItem) => {
|
||||||
|
listItem.addEventListener('pointerdown', () => {
|
||||||
|
input.value = listItem.dataset.value as string
|
||||||
|
input.dispatchEvent(new Event('change'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
47
src/components/global/Table/Table.astro
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import { objectClone } from '@dzeio/object-util'
|
||||||
|
import type TableProps from './TableProps'
|
||||||
|
export interface Props extends TableProps {}
|
||||||
|
|
||||||
|
const props = objectClone(Astro.props)
|
||||||
|
delete props.header
|
||||||
|
delete props.data
|
||||||
|
---
|
||||||
|
<table {...props}>
|
||||||
|
<thead>
|
||||||
|
<tr data-row="0">
|
||||||
|
{Astro.props.header?.map((it, idx) => <th data-cell={idx}>{it}</th>)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Astro.props.data?.map((row, rowIdx) => <tr data-row={rowIdx}>{row.map((it, cellIdx) => <td data-cell={cellIdx}>{it}</td>)}</tr>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
table {
|
||||||
|
@apply flex w-full flex-col border-1 rounded-lg overflow-clip
|
||||||
|
}
|
||||||
|
table :global(th) {
|
||||||
|
@apply font-medium
|
||||||
|
}
|
||||||
|
table :global(thead),
|
||||||
|
table :global(tbody),
|
||||||
|
table :global(tr) {
|
||||||
|
@apply flex justify-between
|
||||||
|
}
|
||||||
|
table :global(thead),
|
||||||
|
table :global(tbody) {
|
||||||
|
@apply flex-col
|
||||||
|
}
|
||||||
|
table :global(th),
|
||||||
|
table :global(td) {
|
||||||
|
@apply block w-full py-2 px-4 text-right
|
||||||
|
}
|
||||||
|
table :global(tr) {
|
||||||
|
@apply border-gray-200
|
||||||
|
}
|
||||||
|
table :global(thead) {
|
||||||
|
@apply bg-gray-100 border-b-1 border-gray-200
|
||||||
|
}
|
||||||
|
</style>
|
4
src/components/global/Table/TableProps.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default interface TableProps extends astroHTML.JSX.TableHTMLAttributes {
|
||||||
|
header?: Array<string | number> | null | undefined
|
||||||
|
data?: Array<Array<string | number>> | null | undefined
|
||||||
|
}
|
95
src/components/global/Table/TableUtil.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import type Props from './TableProps'
|
||||||
|
|
||||||
|
export function updateTable(comp: HTMLTableElement, data: Props, options?: {
|
||||||
|
keepHeaders?: boolean
|
||||||
|
keepData?: boolean
|
||||||
|
}) {
|
||||||
|
const head = comp.querySelector('thead > tr')
|
||||||
|
const body = comp.querySelector('tbody')
|
||||||
|
|
||||||
|
if (!head || !body) {
|
||||||
|
console.error('could not update table')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const curHeaders = head.querySelectorAll('th')
|
||||||
|
const newHeaders = data.header ?? []
|
||||||
|
const headersLength = Math.max(newHeaders.length, curHeaders?.length ?? 0)
|
||||||
|
|
||||||
|
for (let headerIdx = 0; headerIdx < headersLength; headerIdx++) {
|
||||||
|
const headerHTML = curHeaders[headerIdx]
|
||||||
|
const headerContent = newHeaders[headerIdx]
|
||||||
|
// new el, add it
|
||||||
|
if (!headerHTML) {
|
||||||
|
const el = document.createElement('th')
|
||||||
|
el.innerText = headerContent as string
|
||||||
|
el.dataset.idx = headerIdx.toString()
|
||||||
|
head.appendChild(el)
|
||||||
|
// header too large, remove
|
||||||
|
} else if (!headerContent && !options?.keepHeaders) {
|
||||||
|
head.removeChild(headerHTML)
|
||||||
|
// replace content
|
||||||
|
} else if(!options?.keepHeaders) {
|
||||||
|
headerHTML.innerText = (headerContent ?? '').toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const curBody = body.querySelectorAll('tr')
|
||||||
|
const newBody = data.data ?? []
|
||||||
|
const bodyLength = Math.max(newBody.length, curBody.length ?? 0)
|
||||||
|
|
||||||
|
for (let bodyRowIdx = 0; bodyRowIdx < bodyLength; bodyRowIdx++) {
|
||||||
|
const bodyRowHTML = curBody[bodyRowIdx]
|
||||||
|
const bodyRowContent = newBody[bodyRowIdx]
|
||||||
|
// new el, add it
|
||||||
|
if (!bodyRowHTML) {
|
||||||
|
const row = document.createElement('tr')
|
||||||
|
row.dataset.row = bodyRowIdx.toString()
|
||||||
|
for (let cellIdx = 0; cellIdx < (bodyRowContent as Array<string>).length; cellIdx++) {
|
||||||
|
const cellContent = (bodyRowContent as Array<string>)[cellIdx] as string
|
||||||
|
const cell = document.createElement('td')
|
||||||
|
cell.dataset.cell = cellIdx.toString()
|
||||||
|
cell.innerText = cellContent
|
||||||
|
row.appendChild(cell)
|
||||||
|
}
|
||||||
|
body.appendChild(row)
|
||||||
|
// body too large, remove row
|
||||||
|
} else if (!bodyRowContent) {
|
||||||
|
body.removeChild(bodyRowHTML)
|
||||||
|
// replace row
|
||||||
|
} else {
|
||||||
|
const bodyRowHTML = curBody[bodyRowIdx] as HTMLTableRowElement
|
||||||
|
const cells = bodyRowHTML!.querySelectorAll('td')
|
||||||
|
const cellLength = Math.max(cells.length, bodyRowContent.length)
|
||||||
|
for (let cellIdx = 0; cellIdx < cellLength; cellIdx++) {
|
||||||
|
const currCell = cells[cellIdx];
|
||||||
|
const newCell = bodyRowContent[cellIdx];
|
||||||
|
// new el, add it
|
||||||
|
if (!currCell) {
|
||||||
|
const el = document.createElement('td')
|
||||||
|
el.dataset.cell = cellIdx.toString()
|
||||||
|
el.innerText = newCell as string
|
||||||
|
bodyRowHTML.appendChild(el)
|
||||||
|
// header too large, remove
|
||||||
|
} else if (!newCell && !options?.keepData) {
|
||||||
|
bodyRowHTML.removeChild(currCell)
|
||||||
|
// replace content
|
||||||
|
} else if(!options?.keepData) {
|
||||||
|
currCell.innerText = (newCell ?? '').toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOnTableClick(table: HTMLTableElement, fn: (row: number, cell: number) => void | Promise<void>) {
|
||||||
|
table.querySelector('tbody')?.classList?.add('hover:cursor-pointer')
|
||||||
|
table.querySelectorAll<HTMLTableCellElement>('td').forEach((it) => {
|
||||||
|
it.addEventListener('click', () => {
|
||||||
|
const row = it.parentElement as HTMLTableRowElement
|
||||||
|
const rowIdx = parseInt(row.dataset.row as string)
|
||||||
|
const cellIdx = parseInt(it.dataset.cell as string)
|
||||||
|
fn(rowIdx, cellIdx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
26
src/components/layouts/Footer.astro
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
const year = new Date().getFullYear()
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
links?: Array<{href: string, target?: string, display: string}>
|
||||||
|
socials?: Array<{href: string, target?: string, icon: any}>
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="w-full flex flex-col bg-white dark:bg-gray-900 mt-32 py-16 px-1 gap-8">
|
||||||
|
{Astro.props.links && (
|
||||||
|
<div class="w-full flex justify-center gap-6">
|
||||||
|
{Astro.props.links.map((it) => (
|
||||||
|
<a href={it.href} target={it.target ?? "_blank noreferrer nofollow"}>{it.display}</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{Astro.props.socials && (
|
||||||
|
<div class="flex flex-row w-full justify-center gap-6">
|
||||||
|
{Astro.props.socials.map((it) => (
|
||||||
|
<a href={it.href} target={it.target ?? "_blank noreferrer nofollow"}><it.icon /></a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p class="font-light text-center">© {year} <a href="https://www.dzeio.com">Dzeio</a>. Tout droits réservé.</p>
|
||||||
|
</footer>
|
38
src/components/layouts/Header.astro
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
import Logo from 'assets/components/layouts/Header/logo.svg'
|
||||||
|
import Picture from 'components/global/Picture.astro'
|
||||||
|
import Button from 'components/global/Button.astro'
|
||||||
|
import { objectMap } from '@dzeio/object-util'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
right?: Record<string, string>
|
||||||
|
left?: Record<string, string>
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="bg-white/1 z-10 justify-center sm:justify-normal transition-opacity
|
||||||
|
fixed w-full h-20 flex items-center border-b border-slate-900/10 backdrop-blur-md">
|
||||||
|
<nav class="container inline-flex w-full gap-6 items-center justify-between">
|
||||||
|
<div class="inline-flex gap-6 items-center">
|
||||||
|
<a href="/">
|
||||||
|
<Picture src={Logo} alt="Website main logo" class="h-12" />
|
||||||
|
</a>
|
||||||
|
{objectMap(Astro.props.left ?? {}, (path, text) => (
|
||||||
|
<div>
|
||||||
|
<Button type='ghost' href={path}>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div class="inline-flex gap-6 items-center">
|
||||||
|
{objectMap(Astro.props.right ?? {}, (path, text) => (
|
||||||
|
<div>
|
||||||
|
<Button type='ghost' href={path}>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
@ -1,15 +0,0 @@
|
|||||||
// 1. Import utilities from `astro:content`
|
|
||||||
// import { defineCollection, z } from 'astro:content'
|
|
||||||
|
|
||||||
// 2. Define your collection(s)
|
|
||||||
// const docsCollection = defineCollection({
|
|
||||||
// type: 'content',
|
|
||||||
// schema: z.object({
|
|
||||||
// title: z.string()
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// 3. Export a single `collections` object to register your collection(s)
|
|
||||||
// This key should match your collection directory name in "src/content"
|
|
||||||
// export const collections = {
|
|
||||||
// 'docs': docsCollection,
|
|
||||||
// };
|
|
29
src/content/config.ts.tmp
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// 1. Import utilities from `astro:content`
|
||||||
|
import { defineCollection, z } from 'astro:content'
|
||||||
|
|
||||||
|
// 2. Define your collection(s)
|
||||||
|
const projectsCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: ({ image }) => z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
image: image().optional(),
|
||||||
|
link: z.object({
|
||||||
|
href: z.string(),
|
||||||
|
rel: z.string().optional(),
|
||||||
|
text: z.string().optional(),
|
||||||
|
target: z.string().optional()
|
||||||
|
}).optional(),
|
||||||
|
disabled: z.string().optional(),
|
||||||
|
created: z.date().optional(),
|
||||||
|
updated: z.date().optional(),
|
||||||
|
techs: z.string().array().optional()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 3. Export a single `collections` object to register your collection(s)
|
||||||
|
// This key should match your collection directory name in "src/content"
|
||||||
|
export const collections = {
|
||||||
|
projects: projectsCollection
|
||||||
|
}
|
5
src/env.d.ts
vendored
@ -1,6 +1,5 @@
|
|||||||
/// <reference path="../.astro/types.d.ts" />
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
/// <reference types="astro/client" />
|
/// <reference types="astro/client" />
|
||||||
/// <reference path="./libs/ResponseBuilder" />
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Environment variables declaration
|
* Environment variables declaration
|
||||||
@ -17,7 +16,5 @@ declare namespace App {
|
|||||||
/**
|
/**
|
||||||
* Middlewares variables
|
* Middlewares variables
|
||||||
*/
|
*/
|
||||||
interface Locals {
|
interface Locals {}
|
||||||
responseBuilder: ResponseBuilder
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,18 @@
|
|||||||
---
|
---
|
||||||
export interface Props {
|
import Head, { type Props as HeadProps } from './Head.astro'
|
||||||
title: string
|
|
||||||
|
export interface Props extends HeadProps {
|
||||||
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
import Favicon from '../components/Favicon/Favicon.astro'
|
|
||||||
import IconSVG from '../assets/layouts/Base/favicon.svg'
|
|
||||||
import IconPNG from '../assets/layouts/Base/favicon.png'
|
|
||||||
|
|
||||||
const { title } = Astro.props;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<Head {...Astro.props} />
|
||||||
<meta name="description" content="Astro description">
|
<slot name="head" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<!-- Analytics -->
|
|
||||||
<script defer data-domain="example.com" src="/js/script.js"></script>
|
|
||||||
|
|
||||||
<Favicon svg={IconSVG} png={IconPNG} icoPath="/favicon.ico" />
|
|
||||||
<title>{title}</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50">
|
<body class:list={["bg-primary-50 dark:bg-slate-950 text-slate-950 dark:text-primary-50", Astro.props.class]}>
|
||||||
<slot />
|
<slot />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
132
src/layouts/Head.astro
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
import Favicon from 'components/layouts/Favicon/Favicon.astro'
|
||||||
|
import IconSVG from 'assets/layouts/Head/favicon.svg'
|
||||||
|
import IconPNG from 'assets/layouts/Head/favicon.png'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
/**
|
||||||
|
* Site display name
|
||||||
|
*/
|
||||||
|
siteName?: string | undefined
|
||||||
|
/**
|
||||||
|
* Page Title
|
||||||
|
*/
|
||||||
|
title?: string | undefined
|
||||||
|
/**
|
||||||
|
* Page description
|
||||||
|
*/
|
||||||
|
description?: string | undefined
|
||||||
|
/**
|
||||||
|
* define the cannonical url
|
||||||
|
*/
|
||||||
|
canonical?: string | false | undefined
|
||||||
|
/**
|
||||||
|
* OpenGraph image(s)
|
||||||
|
*/
|
||||||
|
image?: typeof IconPNG | Array<typeof IconPNG> | undefined
|
||||||
|
/**
|
||||||
|
* Twitter/X Specific options
|
||||||
|
*/
|
||||||
|
twitter?: {
|
||||||
|
title?: string | undefined
|
||||||
|
card?: "summary" | "summary_large_image" | "app" | "player" | undefined
|
||||||
|
site?: string | undefined
|
||||||
|
creator?: string | undefined
|
||||||
|
} | undefined
|
||||||
|
/**
|
||||||
|
* OpenGraph Specific options (override defaults set by other options)
|
||||||
|
*/
|
||||||
|
og?: {
|
||||||
|
title?: string | undefined
|
||||||
|
type?: string | undefined
|
||||||
|
description?: string | undefined
|
||||||
|
url?: string | undefined
|
||||||
|
} | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = Astro.props
|
||||||
|
|
||||||
|
const image = props.image ? Array.isArray(props.image) ? props.image : [props.image] : undefined
|
||||||
|
|
||||||
|
const canonical = typeof Astro.props.canonical === 'string' ? Astro.props.canonical : Astro.props.canonical === false ? undefined : Astro.url.href
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Charset -->
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
|
||||||
|
<!-- Viewport -->
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Analytics -->
|
||||||
|
<script defer data-domain="avior.me" src="/js/script.js"></script>
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<Favicon svg={IconSVG} png={IconPNG} icoPath="/favicon.ico" />
|
||||||
|
|
||||||
|
<!-- OpenGraph Sitename -->
|
||||||
|
{props.siteName && (
|
||||||
|
<meta property="og:site_name" content={props.siteName} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
{props.title && (
|
||||||
|
<title>{props.title}</title>
|
||||||
|
// <meta property="twitter:title" content={props.twitter?.title ?? props.title} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
{props.description && (
|
||||||
|
<meta name="description" content={props.description} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Canonical -->
|
||||||
|
{canonical && (
|
||||||
|
<link rel="canonical" href={canonical} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta property="twitter:card" content={props.twitter?.card ?? 'summary'} />
|
||||||
|
|
||||||
|
<!-- Twitter Site -->
|
||||||
|
{props.twitter?.site && (
|
||||||
|
<meta property="twitter:site" content={props.twitter.site} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Twitter Creator -->
|
||||||
|
{props.twitter?.creator && (
|
||||||
|
<meta property="twitter:creator" content={props.twitter.creator} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Twitter Title -->
|
||||||
|
{(props.twitter?.title ?? props.title) && (
|
||||||
|
<meta property="twitter:title" content={props.twitter?.title ?? props.title} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- OpenGraph -->
|
||||||
|
<!-- OpenGraph Title -->
|
||||||
|
{(props.og?.title ?? props.title) && (
|
||||||
|
<meta property="og:title" content={props.og?.title ?? props.title} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- OpenGraph Description -->
|
||||||
|
{(props.og?.description ?? props.description) && (
|
||||||
|
<meta property="og:description" content={props.og?.description ?? props.description} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- OpenGraph Type -->
|
||||||
|
<meta property="og:type" content={props.og?.type ?? 'website'} />
|
||||||
|
|
||||||
|
<!-- OpenGraph URL -->
|
||||||
|
{(props.og?.url ?? canonical) && (
|
||||||
|
<meta property="og:url" content={props.og?.url ?? canonical} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- OpenGraph Image -->
|
||||||
|
{image?.map((img) => (
|
||||||
|
<meta property="og:image" content={img.src} />
|
||||||
|
<meta property="og:image:type" content={`image/${img.format}`} />
|
||||||
|
<meta property="og:image:width" content={img.width.toString()} />
|
||||||
|
<meta property="og:image:height" content={img.height.toString()} />
|
||||||
|
))}
|
@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
import Base, { type Props as BaseProps } from './Base.astro'
|
|
||||||
|
|
||||||
export interface Props extends BaseProps {}
|
|
||||||
---
|
|
||||||
|
|
||||||
<Base {...Astro.props}>
|
|
||||||
<main class="container">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</Base>
|
|
30
src/layouts/MainLayout.astro
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import Footer from 'components/layouts/Footer.astro'
|
||||||
|
import Base, { type Props as BaseProps } from './Base.astro'
|
||||||
|
import Header from 'components/layouts/Header.astro'
|
||||||
|
import { Mail, Phone } from 'lucide-astro'
|
||||||
|
import { Github, Linkedin } from 'simple-icons-astro'
|
||||||
|
|
||||||
|
export interface Props extends BaseProps {
|
||||||
|
/**
|
||||||
|
* remove the default top padding top allow the content to overflow with the header
|
||||||
|
*/
|
||||||
|
hasHero?: boolean
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Base {...Astro.props}>
|
||||||
|
<slot slot="head" name="head" />
|
||||||
|
<Header />
|
||||||
|
<div class:list={{'pt-24': !Astro.props.hasHero}}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<Footer links={[
|
||||||
|
{href: 'https://www.avior.me', display: 'About'}
|
||||||
|
]} socials={[
|
||||||
|
{href: 'https://linkedin.com', icon: Linkedin},
|
||||||
|
{href: 'mailto:contact@example.com', target: '', icon: Mail},
|
||||||
|
{href: 'tel:+33601020304', target: '', icon: Phone},
|
||||||
|
{href: 'https://github.com/dzeiocom', icon: Github},
|
||||||
|
]} />
|
||||||
|
</Base>
|
53
src/libs/Component.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
type Fn<T extends HTMLElement> = (el: Component<T>) => void | Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component client side initialisation class
|
||||||
|
*/
|
||||||
|
export default class Component<T extends HTMLElement> {
|
||||||
|
private constructor(
|
||||||
|
public element: T
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public handled(value: boolean): this
|
||||||
|
public handled(): boolean
|
||||||
|
public handled(value?: boolean): this | boolean {
|
||||||
|
if (typeof value === 'undefined') {
|
||||||
|
return typeof this.element.dataset.handled === 'string'
|
||||||
|
}
|
||||||
|
this.element.dataset.handled = ''
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(fn: (el: Component<T>) => void | Promise<void>) {
|
||||||
|
if (this.handled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(this)
|
||||||
|
this.handled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public child<El extends HTMLElement>(query: string, fn: Fn<El>) {
|
||||||
|
this.element.querySelectorAll<El>(query).forEach((it) => {
|
||||||
|
const cp = new Component(it)
|
||||||
|
cp.init(fn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start handling an element
|
||||||
|
* @param query the query to get the element
|
||||||
|
* @param fn the function that is run ONCE per elements
|
||||||
|
*/
|
||||||
|
public static handle<T extends HTMLElement>(query: string, fn: (el: T) => void | Promise<void>) {
|
||||||
|
document.querySelectorAll<T>(query).forEach((it) => {
|
||||||
|
const cp = new Component(it)
|
||||||
|
cp.init((it) => fn(it.element))
|
||||||
|
})
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
document.querySelectorAll<T>(query).forEach((it) => {
|
||||||
|
const cp = new Component(it)
|
||||||
|
cp.init((it) => fn(it.element))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,18 @@
|
|||||||
import { objectLoop } from '@dzeio/object-util'
|
import { objectLoop } from '@dzeio/object-util'
|
||||||
|
import StatusCode from './HTTP/StatusCode'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple builde to create a new Response object
|
* Simple builde to create a new Response object
|
||||||
*/
|
*/
|
||||||
export default class ResponseBuilder {
|
export default class ResponseBuilder {
|
||||||
|
|
||||||
|
public static redirect(location: string, statusCode: number = StatusCode.FOUND) {
|
||||||
|
const resp = new ResponseBuilder()
|
||||||
|
resp.addHeader('Location', location)
|
||||||
|
resp.status(statusCode)
|
||||||
|
return resp.build()
|
||||||
|
}
|
||||||
|
|
||||||
private _body: BodyInit | null | undefined
|
private _body: BodyInit | null | undefined
|
||||||
public body(body: string | Buffer | object | null | undefined) {
|
public body(body: string | Buffer | object | null | undefined) {
|
||||||
if (typeof body === 'object' && !(body instanceof Buffer)) {
|
if (typeof body === 'object' && !(body instanceof Buffer)) {
|
||||||
@ -42,7 +50,7 @@ export default class ResponseBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _status?: number
|
private _status?: number
|
||||||
public status(status: number) {
|
public status(status: StatusCode | number) {
|
||||||
this._status = status
|
this._status = status
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { sequence } from "astro/middleware"
|
import { sequence } from "astro/middleware"
|
||||||
|
|
||||||
import responseBuilder from './responseBuilder'
|
import logger from './logger'
|
||||||
|
|
||||||
export const onRequest = sequence(responseBuilder)
|
export const onRequest = sequence(logger)
|
||||||
|
14
src/middleware/logger.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineMiddleware } from "astro/middleware"
|
||||||
|
import { buildRFC7807 } from '../libs/RFCs/RFC7807'
|
||||||
|
import ResponseBuilder from '../libs/ResponseBuilder'
|
||||||
|
|
||||||
|
// `context` and `next` are automatically typed
|
||||||
|
export default defineMiddleware(async ({ request }, next) => {
|
||||||
|
const prefix = `[${new Date().toISOString()}] ${request.headers.get('user-agent')?.slice(0, 32).padEnd(32)} ${request.method.padEnd(7)}`
|
||||||
|
console.log(`${prefix} ${request.url}`)
|
||||||
|
|
||||||
|
// can crash if response crash
|
||||||
|
const res = await next()
|
||||||
|
console.log(`${prefix} ${res.status} ${request.url}`)
|
||||||
|
return res
|
||||||
|
})
|
@ -1,21 +0,0 @@
|
|||||||
import { defineMiddleware } from "astro/middleware"
|
|
||||||
import { buildRFC7807 } from '../libs/RFCs/RFC7807'
|
|
||||||
import ResponseBuilder from '../libs/ResponseBuilder'
|
|
||||||
|
|
||||||
// `context` and `next` are automatically typed
|
|
||||||
export default defineMiddleware(async ({ request, locals }, next) => {
|
|
||||||
locals.responseBuilder = new ResponseBuilder()
|
|
||||||
console.log(`[${new Date().toISOString()}] ${request.headers.get('user-agent')?.slice(0, 32).padEnd(32)} ${request.method.padEnd(7)} ${request.url}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await next()
|
|
||||||
console.log(`[${new Date().toISOString()}] ${request.headers.get('user-agent')?.slice(0, 32).padEnd(32)} ${request.method.padEnd(7)} ${res.status} ${request.url}`)
|
|
||||||
return res
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
return buildRFC7807({
|
|
||||||
type: '/docs/errors/global-error',
|
|
||||||
status: 500
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
@ -11,7 +11,7 @@ export default abstract class Dao<Object extends { id: any } = { id: any }> {
|
|||||||
* @param obj the object to create
|
* @param obj the object to create
|
||||||
* @returns the object with it's id filled if create or null otherwise
|
* @returns the object with it's id filled if create or null otherwise
|
||||||
*/
|
*/
|
||||||
abstract create(obj: Omit<Object, 'id'>): Promise<Object | null>
|
abstract create(obj: Omit<Object, 'id' | 'created' | 'updated'>): Promise<Object | null>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* insert a new object into the source
|
* insert a new object into the source
|
||||||
@ -81,12 +81,24 @@ export default abstract class Dao<Object extends { id: any } = { id: any }> {
|
|||||||
*/
|
*/
|
||||||
abstract update(obj: Object): Promise<Object | null>
|
abstract update(obj: Object): Promise<Object | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* change some elements from the object and return the object updated
|
||||||
|
* @param id the id of the object
|
||||||
|
* @param changegs the change to make
|
||||||
|
*/
|
||||||
|
public async patch(id: string, changes: Partial<Object>): Promise<Object | null> {
|
||||||
|
const query = await this.findById(id)
|
||||||
|
if (!query) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return await this.update({...query, ...changes})
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* update the remote reference of the object or create it if not found
|
* update the remote reference of the object or create it if not found
|
||||||
* @param obj the object to update/insert
|
* @param obj the object to update/insert
|
||||||
* @returns the object is updated/inserted or null otherwise
|
* @returns the object is updated/inserted or null otherwise
|
||||||
*/
|
*/
|
||||||
public async upsert(object: Object | Omit<Object, 'id'>): Promise<Object | null> {
|
public async upsert(object: Object | Omit<Object, 'id' | 'created' | 'updated'>): Promise<Object | null> {
|
||||||
if ('id' in object) {
|
if ('id' in object) {
|
||||||
return this.update(object)
|
return this.update(object)
|
||||||
}
|
}
|
||||||
|
24
src/pages/404.astro
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import MainLayout from 'layouts/MainLayout.astro'
|
||||||
|
import I404 from 'assets/pages/404/404.svg'
|
||||||
|
import I404Light from 'assets/pages/404/404.light.svg'
|
||||||
|
import Button from 'components/global/Button.astro'
|
||||||
|
import Picture from 'components/global/Picture.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout>
|
||||||
|
<main class="container flex flex-col gap-24 justify-center items-center md:mt-6">
|
||||||
|
<h1 class="text-6xl text-center font-bold">404 La page recherché n'existe pas :(</h1>
|
||||||
|
<Picture src={I404Light} srcDark={I404} alt="404 error image" />
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<Button href="/">Retour à la page d'accueil</Button>
|
||||||
|
<Button id="back_button">Retour à la page précédente</Button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</MainLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(document.querySelector('button.back_button') as HTMLButtonElement).addEventListener('click', () => {
|
||||||
|
history.back()
|
||||||
|
})
|
||||||
|
</script>
|
23
src/pages/api/event.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import ResponseBuilder from '../../libs/ResponseBuilder'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plausible proxy
|
||||||
|
*/
|
||||||
|
export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||||
|
// const body = await request.json()
|
||||||
|
// console.log(body, clientAddress)
|
||||||
|
const res = await fetch('https://plausible.io/api/event', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': request.headers.get('User-Agent') as string,
|
||||||
|
'X-Forwarded-For': clientAddress,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: await request.text()
|
||||||
|
})
|
||||||
|
return new ResponseBuilder()
|
||||||
|
.status(res.status)
|
||||||
|
.body(await res.text())
|
||||||
|
.build()
|
||||||
|
}
|
23
src/pages/event.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import ResponseBuilder from '../../libs/ResponseBuilder'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plausible proxy
|
||||||
|
*/
|
||||||
|
export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||||
|
// const body = await request.json()
|
||||||
|
// console.log(body, clientAddress)
|
||||||
|
const res = await fetch('https://plausible.io/api/event', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': request.headers.get('User-Agent') as string,
|
||||||
|
'X-Forwarded-For': clientAddress,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: await request.text()
|
||||||
|
})
|
||||||
|
return new ResponseBuilder()
|
||||||
|
.status(res.status)
|
||||||
|
.body(await res.text())
|
||||||
|
.build()
|
||||||
|
}
|
@ -1,13 +1,10 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro'
|
import MainLayout from 'layouts/MainLayout.astro'
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Welcome to Astro.">
|
<MainLayout title="Dzeio - Website Template">
|
||||||
<main>
|
<main class="container flex flex-col justify-center items-center h-screen gap-6">
|
||||||
<h1>Welcome to <span class="text-gradient">Astro</span></h1>
|
<h1 class="text-8xl text-center">Dzeio Astro Template</h1>
|
||||||
<p class="instructions">
|
<h2 class="text-2xl text-center">Start editing src/pages/index.astro to see your changes!</h2>
|
||||||
To get started, open the directory <code>src/pages</code> in your project.<br />
|
|
||||||
<strong>Code Challenge:</strong> Tweak the "Welcome to Astro" message above.
|
|
||||||
</p>
|
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</MainLayout>
|
||||||
|
13
src/pages/js/script.js.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import ResponseBuilder from '../../libs/ResponseBuilder'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plausible proxy
|
||||||
|
*/
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
const res = await fetch('https://plausible.io/js/script.outbound-links.tagged-events.js')
|
||||||
|
return new ResponseBuilder()
|
||||||
|
.status(200)
|
||||||
|
.body(await res.text())
|
||||||
|
.build()
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
// const defaultTheme = require('tailwindcss/defaultTheme')
|
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||||
|
const colors = require('tailwindcss/colors')
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|