feat: Upgrade template based on projects made with it
Some checks failed
Build, check & Test / run (push) Failing after 42s
Some checks failed
Build, check & Test / run (push) Failing after 42s
Signed-off-by: Florian Bouillon <f.bouillon@aptatio.com>
This commit is contained in:
28
src/components/global/Breadcrumb.astro
Normal file
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
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
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 AstroUtils from '../libs/AstroUtils'
|
||||
import AstroUtils from '../../libs/AstroUtils'
|
||||
import { objectOmit } from '@dzeio/object-util'
|
||||
|
||||
const formats = [
|
30
src/components/global/Range.astro
Normal file
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
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
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
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
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
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
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>
|
Reference in New Issue
Block a user