generated from avior/template-web-astro
Initial commit
This commit is contained in:
commit
b7c5f5148e
17
.dockerignore
Normal file
17
.dockerignore
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
15
.editorconfig
Normal file
15
.editorconfig
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 120
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
|
# Yaml Standard
|
||||||
|
[*.{yaml,yml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
35
.github/workflows/build_and_check.yml
vendored
Normal file
35
.github/workflows/build_and_check.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: Build, check & Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js 20
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
run: npm run check
|
||||||
|
|
||||||
|
- name: Prepare Tests
|
||||||
|
run: npm run install:test
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run : npm run test
|
71
.github/workflows/build_docker.yml
vendored
Normal file
71
.github/workflows/build_docker.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
name: Build Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ env.IMAGE_NAME }}
|
||||||
|
# ghcr.io/${{ env.IMAGE_NAME }}
|
||||||
|
# git.example.com/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=edge
|
||||||
|
type=ref,event=pr
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=semver,pattern=latest
|
||||||
|
|
||||||
|
# - name: Login to a private registry
|
||||||
|
# uses: docker/login-action@v3
|
||||||
|
# with:
|
||||||
|
# registry: git.example.com
|
||||||
|
# username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
# password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
# - name: Login to the Docker Hub
|
||||||
|
# uses: docker/login-action@v3
|
||||||
|
# with:
|
||||||
|
# username: ${{ secrets.DOCKER_USER }}
|
||||||
|
# password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
# - name: Login to the Github Packages
|
||||||
|
# uses: docker/login-action@v3
|
||||||
|
# with:
|
||||||
|
# registry: ghcr.io
|
||||||
|
# username: ${{ github.actor }}
|
||||||
|
# password: ${{ secrets.GITHUB_PACKAGES_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push the project
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
file: ./Dockerfile
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
44
.github/workflows/delete_packages.yml
vendored
Normal file
44
.github/workflows/delete_packages.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
name: Delete Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Run when a PR is closed/merged
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
# Run when a branch is deleted
|
||||||
|
delete:
|
||||||
|
|
||||||
|
# Config
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||||
|
GROUP: avior
|
||||||
|
TYPE: container
|
||||||
|
PACKAGE: ifremer-loop
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pull_request:
|
||||||
|
name: Delete the package on a closed Pull Request
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'pull_request' && github.event.action == 'close' }}
|
||||||
|
steps:
|
||||||
|
- name: Delete package
|
||||||
|
run: |
|
||||||
|
curl --fail -X 'DELETE' "https://git.dzeio.com/api/v1/packages/${GROUP}/${TYPE}/${PACKAGE}/${VERSION}" -H 'accept: application/json' -H "Authorization: token ${TOKEN}"
|
||||||
|
env:
|
||||||
|
VERSION: pr-${{ github.event.number }}-head
|
||||||
|
branch:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Delete the package on a deleted branch
|
||||||
|
if: ${{ github.event_name == 'delete' }}
|
||||||
|
steps:
|
||||||
|
- name: Delete package
|
||||||
|
run: |
|
||||||
|
curl --fail -X 'DELETE' "https://git.dzeio.com/api/v1/packages/${GROUP}/${TYPE}/${PACKAGE}/${VERSION}" -H 'accept: application/json' -H "Authorization: token ${TOKEN}"
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||||
|
GROUP: avior
|
||||||
|
TYPE: container
|
||||||
|
PACKAGE: ifremer-loop
|
||||||
|
VERSION: branch-${{ github.event.ref }}
|
||||||
|
|
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
slicers/*
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/playwright/
|
||||||
|
|
||||||
|
/src/route.ts
|
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"editor.quickSuggestions": {
|
||||||
|
"strings": "on"
|
||||||
|
},
|
||||||
|
"tailwindCSS.includeLanguages": {
|
||||||
|
"astro": "html"
|
||||||
|
}
|
||||||
|
}
|
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# This Dockerfile allows you to run AstroJS in server mode
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Build #
|
||||||
|
#########
|
||||||
|
FROM docker.io/node:20-alpine as BUILD_IMAGE
|
||||||
|
|
||||||
|
# run as non root user
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# go to user repository
|
||||||
|
WORKDIR /home/node
|
||||||
|
|
||||||
|
# Add package json
|
||||||
|
ADD --chown=node:node package.json package-lock.json ./
|
||||||
|
|
||||||
|
# install dependencies from package lock
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Add project files
|
||||||
|
ADD --chown=node:node . .
|
||||||
|
|
||||||
|
# build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# remove dev deps
|
||||||
|
RUN npm prune --omit=dev
|
||||||
|
|
||||||
|
##############
|
||||||
|
# Production #
|
||||||
|
##############
|
||||||
|
FROM docker.io/node:20-alpine as PROD_IMAGE
|
||||||
|
|
||||||
|
# inform software to be in production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
|
# run as non root user
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# go to work folder
|
||||||
|
WORKDIR /home/node
|
||||||
|
|
||||||
|
# copy from build image
|
||||||
|
COPY --chown=node:node --from=BUILD_IMAGE /home/node/node_modules ./node_modules
|
||||||
|
COPY --chown=node:node --from=BUILD_IMAGE /home/node/dist ./dist
|
||||||
|
COPY --chown=node:node --from=BUILD_IMAGE /home/node/package.json /home/node/.env* ./
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# run it !
|
||||||
|
CMD ["npm", "run", "start"]
|
38
Dockerfile.static
Normal file
38
Dockerfile.static
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# This Dockerfile allows you to run Astro in a static container (no server side)
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Build #
|
||||||
|
#########
|
||||||
|
FROM docker.io/node:20-alpine as BUILD_IMAGE
|
||||||
|
|
||||||
|
# run as non root user
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# go to user repository
|
||||||
|
WORKDIR /home/node
|
||||||
|
|
||||||
|
# Add package json
|
||||||
|
ADD --chown=node:node package.json package-lock.json ./
|
||||||
|
|
||||||
|
# install dependencies from package lock
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Add project files
|
||||||
|
ADD --chown=node:node . .
|
||||||
|
|
||||||
|
# build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
##############
|
||||||
|
# Production #
|
||||||
|
##############
|
||||||
|
FROM docker.io/nginx:1-alpine
|
||||||
|
|
||||||
|
# go to NGINX folder
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy the nginx config
|
||||||
|
ADD ./nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Copy dist fro mthe build image
|
||||||
|
COPY --from=BUILD_IMAGE /home/node/dist ./
|
72
astro.config.mjs
Normal file
72
astro.config.mjs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import tailwind from "@astrojs/tailwind"
|
||||||
|
import node from "@astrojs/node"
|
||||||
|
import routing from './hooks/routing'
|
||||||
|
|
||||||
|
// const faviconHook = {
|
||||||
|
// name: 'Favicon',
|
||||||
|
// hooks: {
|
||||||
|
// "astro:build:setup": async () => {
|
||||||
|
// await Manifest.create('./src/assets/favicon.png', {
|
||||||
|
// name: 'Template'
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
// Use the NodeJS adapter
|
||||||
|
adapter: node({
|
||||||
|
mode: "standalone"
|
||||||
|
}),
|
||||||
|
|
||||||
|
// some settings to the build output
|
||||||
|
build: {
|
||||||
|
// the asset path
|
||||||
|
assets: 'assets',
|
||||||
|
|
||||||
|
// inline the stylesheet if it is small enough
|
||||||
|
inlineStylesheets: 'auto'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Compress the HTML output
|
||||||
|
compressHTML: true,
|
||||||
|
|
||||||
|
// Customizable depending on goal
|
||||||
|
output: 'server',
|
||||||
|
|
||||||
|
// Add TailwindCSS
|
||||||
|
integrations: [tailwind(), routing()],
|
||||||
|
|
||||||
|
// prefetch links
|
||||||
|
prefetch: {
|
||||||
|
defaultStrategy: 'hover'
|
||||||
|
},
|
||||||
|
|
||||||
|
// the site url
|
||||||
|
site: 'https://example.com',
|
||||||
|
|
||||||
|
// the Output server
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
port: 3000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove the trailing slash by default
|
||||||
|
trailingSlash: 'never',
|
||||||
|
|
||||||
|
// Dev Server
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
watch: {
|
||||||
|
// Ignore some paths
|
||||||
|
ignored: [],
|
||||||
|
// support polling and WSL
|
||||||
|
usePolling: !!(process.env.USE_POLLING ?? process.env.WSL_DISTRO_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
})
|
3
e2e/README.md
Normal file
3
e2e/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# e2e
|
||||||
|
|
||||||
|
Hold End 2 End tests
|
8
e2e/example.spec.ts
Normal file
8
e2e/example.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
|
||||||
|
test('has title', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Expect a title "to contain" a substring.
|
||||||
|
await expect(page).toHaveTitle(/Dzeio/);
|
||||||
|
})
|
199
hooks/routing.ts
Normal file
199
hooks/routing.ts
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import type { AstroIntegration } from 'astro'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
|
||||||
|
const baseFile = `
|
||||||
|
import { objectLoop } from '@dzeio/object-util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a Route with \`[param]\` elements with data in them
|
||||||
|
*
|
||||||
|
* limits: currently does not support \`[...param]\`
|
||||||
|
*
|
||||||
|
* @param url {string} the url to format
|
||||||
|
* @param params {Record<string, string | number>} parameters to add to the URL (in [] first else in the query)
|
||||||
|
*
|
||||||
|
* @returns the URL formatted with the params
|
||||||
|
*/
|
||||||
|
export function formatRoute<T extends string>(url: T, params?: Record<string, string | number>): string {
|
||||||
|
let result: string = url
|
||||||
|
|
||||||
|
// early return if there are no params
|
||||||
|
if (!params) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// external queries for the URL
|
||||||
|
let externalQueries = ''
|
||||||
|
|
||||||
|
// loop through the parameters
|
||||||
|
objectLoop(params, (value, key) => {
|
||||||
|
const search = \`[\${key}]\`
|
||||||
|
value = encodeURI(value.toString())
|
||||||
|
if (!result.includes(search)) {
|
||||||
|
externalQueries += \`\${encodeURI(key)}=\${value}&\`
|
||||||
|
} else {
|
||||||
|
result = result.replace(search, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// add items to the external queries if they are set
|
||||||
|
if (externalQueries) {
|
||||||
|
externalQueries = '?' + externalQueries.slice(0, externalQueries.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result + externalQueries
|
||||||
|
}`.trim()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the file that contains every routes
|
||||||
|
*
|
||||||
|
* @param output the output file location
|
||||||
|
*/
|
||||||
|
async function updateRoutes(output: string, routes: Array<string>) {
|
||||||
|
let file = baseFile
|
||||||
|
file += `\n\nexport type Routes = ${routes.map((it) => `'${it}'`).join(' | ')}`
|
||||||
|
|
||||||
|
file += '\n\nexport default function route(route: Routes, query?: Record<string, string | number>) {'
|
||||||
|
file += '\n\treturn formatRoute(route, query)'
|
||||||
|
file += '\n}\n'
|
||||||
|
|
||||||
|
await fs.writeFile(output, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* format the path back to an url usable by the app
|
||||||
|
*
|
||||||
|
* @param path the path to format
|
||||||
|
* @returns the path formatted as a URL
|
||||||
|
*/
|
||||||
|
function formatPath(basePath: string, path: string, removeExtension = true): string {
|
||||||
|
// remove the base path
|
||||||
|
path = path.replace(basePath, '')
|
||||||
|
|
||||||
|
// remove the extension if asked
|
||||||
|
if (removeExtension) {
|
||||||
|
const lastDot = path.lastIndexOf('.')
|
||||||
|
path = path.slice(0, lastDot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the index from the element
|
||||||
|
if (path.endsWith('/index')) {
|
||||||
|
path = path.replace('/index', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the `/` endpoint
|
||||||
|
if (path === '') {
|
||||||
|
path = '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get every files recursivelly in a specific directory
|
||||||
|
*
|
||||||
|
* @param path the path to search
|
||||||
|
* @returns the list of files recursivelly in the specific directory
|
||||||
|
*/
|
||||||
|
async function getFiles(path: string): Promise<Array<string>> {
|
||||||
|
const dir = await fs.readdir(path)
|
||||||
|
let files: Array<string> = []
|
||||||
|
for (const file of dir) {
|
||||||
|
if (file.startsWith('_')) continue
|
||||||
|
const filePath = path + '/' + file
|
||||||
|
if ((await fs.stat(filePath)).isDirectory()) {
|
||||||
|
files = files.concat(await getFiles(filePath))
|
||||||
|
} else {
|
||||||
|
files.push(filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
let publicFolder!: string
|
||||||
|
let srcFolder!: string
|
||||||
|
let pagesFolder!: string
|
||||||
|
let outputFile!: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* launch the integration
|
||||||
|
* @returns the routng integration
|
||||||
|
*/
|
||||||
|
const integration: () => AstroIntegration = () => ({
|
||||||
|
name: 'Routing',
|
||||||
|
hooks: {
|
||||||
|
'astro:config:setup': async ({ config }) => {
|
||||||
|
publicFolder = config.publicDir.toString().replace('file:///', '')
|
||||||
|
srcFolder = config.srcDir.toString().replace('file:///', '')
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
srcFolder = '/' + srcFolder
|
||||||
|
publicFolder = '/' + publicFolder
|
||||||
|
}
|
||||||
|
pagesFolder = srcFolder + 'pages'
|
||||||
|
outputFile = srcFolder + 'route.ts'
|
||||||
|
|
||||||
|
// get the files list
|
||||||
|
const files = (await Promise.all([
|
||||||
|
await getFiles(pagesFolder).then((ev) => ev.map((it) => formatPath(pagesFolder, it))),
|
||||||
|
await getFiles(publicFolder).then((ev) => ev.map((it) => formatPath(publicFolder, it, false)))
|
||||||
|
])).flat()
|
||||||
|
await updateRoutes(outputFile, files)
|
||||||
|
},
|
||||||
|
'astro:server:setup': async ({ server }) => {
|
||||||
|
|
||||||
|
// get the files list
|
||||||
|
const files = (await Promise.all([
|
||||||
|
await getFiles(pagesFolder).then((ev) => ev.map((it) => formatPath(pagesFolder, it))),
|
||||||
|
await getFiles(publicFolder).then((ev) => ev.map((it) => formatPath(publicFolder, it, false)))
|
||||||
|
])).flat()
|
||||||
|
|
||||||
|
// watch FS changes for new files to add them to the route list
|
||||||
|
server.watcher.on('add', (path) => {
|
||||||
|
path = path.replace(/\\/g, '/')
|
||||||
|
|
||||||
|
// ignore files starting with '_'
|
||||||
|
const filename = path.slice(path.lastIndexOf('/') + 1)
|
||||||
|
if (filename.startsWith('_')) return
|
||||||
|
|
||||||
|
let removeExtension = true
|
||||||
|
let folder = pagesFolder
|
||||||
|
if(path.startsWith(publicFolder)) {
|
||||||
|
removeExtension = false
|
||||||
|
folder = publicFolder
|
||||||
|
} else if (!path.startsWith(folder)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// format the path
|
||||||
|
path = formatPath(folder, path, removeExtension)
|
||||||
|
|
||||||
|
// update the router
|
||||||
|
files.push(path)
|
||||||
|
updateRoutes(outputFile, files)
|
||||||
|
})
|
||||||
|
|
||||||
|
// watch FS changes for removed files to remove them from the list
|
||||||
|
server.watcher.on('unlink', (path) => {
|
||||||
|
path = path.replace(/\\/g, '/')
|
||||||
|
let removeExtension = true
|
||||||
|
let folder = pagesFolder
|
||||||
|
if(path.startsWith(publicFolder)) {
|
||||||
|
removeExtension = false
|
||||||
|
folder = publicFolder
|
||||||
|
}
|
||||||
|
path = formatPath(folder, path, removeExtension)
|
||||||
|
|
||||||
|
const index = files.indexOf(path)
|
||||||
|
|
||||||
|
files.splice(index, 1)
|
||||||
|
updateRoutes(outputFile, files)
|
||||||
|
})
|
||||||
|
|
||||||
|
// run the script once
|
||||||
|
await updateRoutes(outputFile, files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default integration
|
74
nginx.conf
Normal file
74
nginx.conf
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 3000;
|
||||||
|
listen [::]:3000;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
error_page 500 502 503 504 /500.html;
|
||||||
|
|
||||||
|
# Security headers (note: temporarely unvailable "prefetch-src 'self'; ")
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; form-action 'self'; manifest-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src data: 'self'; font-src 'self'; connect-src 'self'; base-uri 'self';";
|
||||||
|
add_header X-Frame-Options "DENY";
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
add_header Referrer-Policy "no-referrer";
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), interest-cohort=()";
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
|
||||||
|
add_header X-Download-Options "noopen";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri.html $uri/index.html /$uri /$uri/index.html /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plausible script
|
||||||
|
location /js/script.js {
|
||||||
|
# Change this if you use a different variant of the script
|
||||||
|
proxy_pass https://plausible.io/js/script.js;
|
||||||
|
proxy_set_header Host plausible.io;
|
||||||
|
|
||||||
|
# Tiny, negligible performance improvement. Very optional.
|
||||||
|
proxy_buffering on;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plausible script
|
||||||
|
location /api/event {
|
||||||
|
proxy_pass https://plausible.io/api/event;
|
||||||
|
proxy_set_header Host plausible.io;
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Media: images, icons, video, audio, HTC
|
||||||
|
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|mp3|ogg|ogv|webp|webm|htc|woff2|woff)$ {
|
||||||
|
expires 1y;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# CSS and Javascript
|
||||||
|
location ~* \.(?:css|js)$ {
|
||||||
|
expires 1y;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8356
package-lock.json
generated
Normal file
8356
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@dzeio/template",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "node ./dist/server/entry.mjs",
|
||||||
|
"build": "astro build",
|
||||||
|
"check": "npm run check:astro && npm run check:typescript",
|
||||||
|
"check:astro": "astro check",
|
||||||
|
"check:typescript": "tsc --noEmit",
|
||||||
|
"test": "npm run test:unit && npm run test:e2e",
|
||||||
|
"test:unit": "vitest --coverage --run",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"install:test": "playwright install --with-deps"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/node": "^7",
|
||||||
|
"@astrojs/tailwind": "^5",
|
||||||
|
"@dzeio/logger": "^3",
|
||||||
|
"@dzeio/object-util": "^1",
|
||||||
|
"@dzeio/url-manager": "^1",
|
||||||
|
"astro": "^4",
|
||||||
|
"lucide-astro": "^0",
|
||||||
|
"sharp": "^0",
|
||||||
|
"simple-icons-astro": "^10",
|
||||||
|
"tailwindcss": "^3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "^0",
|
||||||
|
"@playwright/test": "^1",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@vitest/coverage-v8": "^1",
|
||||||
|
"typescript": "^5",
|
||||||
|
"vitest": "^1"
|
||||||
|
}
|
||||||
|
}
|
62
playwright.config.ts
Normal file
62
playwright.config.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run start',
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
timeout: 120 * 1000,
|
||||||
|
reuseExistingServer: !process.env.CI
|
||||||
|
},
|
||||||
|
outputDir: './playwright/results',
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined as any,
|
||||||
|
reporter: process.env.CI ? 'list' : [['html', {
|
||||||
|
outputFolder: './playwright/report',
|
||||||
|
open: 'never'
|
||||||
|
}]],
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
})
|
9
public/favicon.svg
Normal file
9
public/favicon.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 |
7
src/assets/README.md
Normal file
7
src/assets/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Assets
|
||||||
|
|
||||||
|
Contains images that can be imported directly into the application
|
||||||
|
|
||||||
|
# Folder Architecture
|
||||||
|
|
||||||
|
- /assets/[path to element from src]/[folder named as the element]/[assets of the element].[ext]
|
9
src/assets/components/layouts/Header/logo.svg
Normal file
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 |
BIN
src/assets/layouts/Head/favicon.png
Normal file
BIN
src/assets/layouts/Head/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
9
src/assets/layouts/Head/favicon.svg
Normal file
9
src/assets/layouts/Head/favicon.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 |
1
src/assets/pages/404/404.light.svg
Normal file
1
src/assets/pages/404/404.light.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 23 KiB |
1
src/assets/pages/404/404.svg
Normal file
1
src/assets/pages/404/404.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 23 KiB |
4
src/components/Passthrough/Passthrough.astro
Normal file
4
src/components/Passthrough/Passthrough.astro
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
const json = JSON.stringify(Astro.props)
|
||||||
|
---
|
||||||
|
<script id="ASTRO_DATA" is:inline type="application/json" set:html={json}></script>
|
10
src/components/Passthrough/utils.ts
Normal file
10
src/components/Passthrough/utils.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* note: you MUST only pass simple items that can go in JSON format natively
|
||||||
|
*/
|
||||||
|
export function load<T extends {} = {}>(): T {
|
||||||
|
const tag = document.querySelector<HTMLScriptElement>('#ASTRO_DATA')
|
||||||
|
if (!tag) {
|
||||||
|
throw new Error('could not load client variables, tag not found')
|
||||||
|
}
|
||||||
|
return JSON.parse(tag.innerText)
|
||||||
|
}
|
3
src/components/README.md
Normal file
3
src/components/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Components
|
||||||
|
|
||||||
|
Contains big elements that can be reused by themselve
|
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>
|
37
src/components/global/Button.astro
Normal file
37
src/components/global/Button.astro
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
|
|
||||||
|
export interface Props extends astroHTML.JSX.AnchorHTMLAttributes {
|
||||||
|
outline?: boolean
|
||||||
|
ghost?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = [
|
||||||
|
"button",
|
||||||
|
{outline: Astro.props.outline},
|
||||||
|
{ghost: Astro.props.ghost},
|
||||||
|
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>
|
88
src/components/global/Picture.astro
Normal file
88
src/components/global/Picture.astro
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
import { getImage } from 'astro:assets'
|
||||||
|
import AstroUtils from '../../libs/AstroUtils'
|
||||||
|
import { objectOmit } from '@dzeio/object-util'
|
||||||
|
|
||||||
|
const formats = [
|
||||||
|
'avif',
|
||||||
|
'webp'
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface Props extends Omit<astroHTML.JSX.ImgHTMLAttributes, 'src'> {
|
||||||
|
src: ImageMetadata | string
|
||||||
|
srcDark?: ImageMetadata | string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type PictureResult = {
|
||||||
|
format: 'new'
|
||||||
|
formats: Array<{format: string, img: Awaited<ReturnType<typeof getImage>>}>
|
||||||
|
src: Awaited<ReturnType<typeof getImage>>
|
||||||
|
} | {
|
||||||
|
format: 'raw'
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result {
|
||||||
|
light: PictureResult
|
||||||
|
dark?: PictureResult | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolvePicture(image: ImageMetadata | string): Promise<PictureResult> {
|
||||||
|
const ext = typeof image === 'string' ? image.substring(image.lastIndexOf('.')) : image.format
|
||||||
|
if (ext === 'svg') {
|
||||||
|
return {
|
||||||
|
format: 'raw',
|
||||||
|
src: typeof image === 'string' ? image : image.src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageFormats: Array<{format: string, img: Awaited<ReturnType<typeof getImage>>}> = await Promise.all(
|
||||||
|
formats.map(async (it) => ({
|
||||||
|
img: await getImage({src: Astro.props.src, format: it, width: Astro.props.width, height: Astro.props.height}),
|
||||||
|
format: it
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const orig = await getImage({src: Astro.props.src, format: ext, width: Astro.props.width, height: Astro.props.height})
|
||||||
|
|
||||||
|
return {
|
||||||
|
format: 'new',
|
||||||
|
formats: imageFormats,
|
||||||
|
src: orig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await AstroUtils.wrap<Result>(async () => {
|
||||||
|
return {
|
||||||
|
light: await resolvePicture(Astro.props.src),
|
||||||
|
dark: Astro.props.srcDark ? await resolvePicture(Astro.props.srcDark) : undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = objectOmit(Astro.props, 'src', 'srcDark', 'class')
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{res.light.format === 'new' && (
|
||||||
|
<picture {...props} {...res.light.src.attributes} class:list={[res.light.src.attributes.class, Astro.props.class, {'dark:hidden': res.dark}]}>
|
||||||
|
{res.light.formats.map((it) => (
|
||||||
|
<source srcset={it.img.src} type={`image/${it.format}`} />
|
||||||
|
))}
|
||||||
|
<img src={res.light.src.src} />
|
||||||
|
</picture>
|
||||||
|
) || (
|
||||||
|
<img {...props} class:list={[Astro.props.class, {'dark:hidden': res.dark}]} src={res.light.src as string} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{res.dark && res.dark.format === 'new' && (
|
||||||
|
<picture {...props} {...res.dark.src.attributes} class:list={[res.dark.src.attributes.class, Astro.props.class, 'hidden', 'dark:block']}>
|
||||||
|
{res.dark.formats.map((it) => (
|
||||||
|
<source srcset={it.img.src} type={`image/${it.format}`} />
|
||||||
|
))}
|
||||||
|
<img src={res.dark.src.src} />
|
||||||
|
</picture>
|
||||||
|
) || (res.dark && (
|
||||||
|
<img {...props} class:list={[Astro.props.class, 'hidden', 'dark:block']} src={res.dark.src as string} />
|
||||||
|
))}
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
24
src/components/layouts/Favicon/Favicon.astro
Normal file
24
src/components/layouts/Favicon/Favicon.astro
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import { getImage } from 'astro:assets'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
svg: ImageMetadata
|
||||||
|
png: ImageMetadata
|
||||||
|
icoPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Astro.props.icoPath !== '/favicon.ico') {
|
||||||
|
console.warn('It is recommanded that the ICO file should be located at /favicon.ico')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const appleTouch = await getImage({src: Astro.props.png, width: 180, height: 180})
|
||||||
|
---
|
||||||
|
|
||||||
|
<>
|
||||||
|
<link rel="icon" href={Astro.props.icoPath ?? "/favicon.ico"} sizes="any">
|
||||||
|
<link rel="icon" href={Astro.props.svg.src} type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href={appleTouch.src} />
|
||||||
|
<!-- Currently not integrated until I find a way to. -->
|
||||||
|
<!-- <link rel="manifest" href="/site.webmanifest" /> -->
|
||||||
|
</>
|
36
src/components/layouts/Favicon/Manifest.ts
Normal file
36
src/components/layouts/Favicon/Manifest.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { getImage } from 'astro:assets'
|
||||||
|
|
||||||
|
export default class Manifest {
|
||||||
|
static async create(baseImage: ImageMetadata, options: {
|
||||||
|
name: string
|
||||||
|
color?: string
|
||||||
|
images?: Array<number>
|
||||||
|
}) {
|
||||||
|
const [
|
||||||
|
i192,
|
||||||
|
i512
|
||||||
|
] = await Promise.all([
|
||||||
|
getImage({src: baseImage, format: 'png', width: 192, height: 192}),
|
||||||
|
getImage({src: baseImage, format: 'png', width: 512, height: 512})
|
||||||
|
])
|
||||||
|
return JSON.stringify({
|
||||||
|
name: options.name,
|
||||||
|
short_name: options.name,
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: i192.src,
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: i512.src,
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
theme_color: options.color ?? "#fff",
|
||||||
|
background_color: options.color ?? "#fff",
|
||||||
|
display: "standalone"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
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 ghost href={path}>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div class="inline-flex gap-6 items-center">
|
||||||
|
{objectMap(Astro.props.right ?? {}, (path, text) => (
|
||||||
|
<div>
|
||||||
|
<Button ghost href={path}>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
5
src/content/_README.md
Normal file
5
src/content/_README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Content
|
||||||
|
|
||||||
|
Contains raw content for pages.
|
||||||
|
|
||||||
|
Mostly some static pages or blog posts.
|
29
src/content/_config.ts
Normal file
29
src/content/_config.ts
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
|
||||||
|
}
|
20
src/env.d.ts
vendored
Normal file
20
src/env.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variables declaration
|
||||||
|
*/
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
declare namespace App {
|
||||||
|
/**
|
||||||
|
* Middlewares variables
|
||||||
|
*/
|
||||||
|
interface Locals {}
|
||||||
|
}
|
18
src/layouts/Base.astro
Normal file
18
src/layouts/Base.astro
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
import Head, { type Props as HeadProps } from './Head.astro'
|
||||||
|
|
||||||
|
export interface Props extends HeadProps {
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<Head {...Astro.props} />
|
||||||
|
<slot name="head" />
|
||||||
|
</head>
|
||||||
|
<body class:list={["bg-primary-50 dark:bg-slate-950 text-slate-950 dark:text-primary-50", Astro.props.class]}>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
132
src/layouts/Head.astro
Normal file
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()} />
|
||||||
|
))}
|
30
src/layouts/MainLayout.astro
Normal file
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>
|
7
src/layouts/README.md
Normal file
7
src/layouts/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Layouts
|
||||||
|
|
||||||
|
Application different layouts they should extends `Base.astro` if added and also pass the parameters of Base.astro to the page
|
||||||
|
|
||||||
|
## Base.astro
|
||||||
|
|
||||||
|
This is the base file for each path of the application, executed for each paths
|
5
src/libs/AstroUtils.ts
Normal file
5
src/libs/AstroUtils.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default class AstroUtils {
|
||||||
|
public static async wrap<T = void>(fn: () => T | Promise<T>) {
|
||||||
|
return await fn()
|
||||||
|
}
|
||||||
|
}
|
53
src/libs/Component.ts
Normal file
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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
289
src/libs/HTTP/StatusCode.ts
Normal file
289
src/libs/HTTP/StatusCode.ts
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
/**
|
||||||
|
* HTTP Status code
|
||||||
|
*
|
||||||
|
* Following https://developer.mozilla.org/en-US/docs/Web/HTTP/Status an extension of the RFC9110
|
||||||
|
*/
|
||||||
|
enum StatusCode {
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 1xx Requests *
|
||||||
|
****************/
|
||||||
|
/**
|
||||||
|
* This interim response indicates that the client should continue the request or ignore the response if the request is already finished.
|
||||||
|
*/
|
||||||
|
CONTINUE = 100,
|
||||||
|
/**
|
||||||
|
* This code is sent in response to an Upgrade request header from the client and indicates the protocol the server is switching to.
|
||||||
|
*/
|
||||||
|
SWITCHING_PROTOCOLS,
|
||||||
|
/**
|
||||||
|
* This code indicates that the server has received and is processing the request, but no response is available yet.
|
||||||
|
*/
|
||||||
|
PROCESSING,
|
||||||
|
/**
|
||||||
|
* This status code is primarily intended to be used with the Link header, letting the user agent start preloading resources while the server prepares a response.
|
||||||
|
*/
|
||||||
|
EARLY_HINTS,
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 2xx Requests *
|
||||||
|
****************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request succeeded. The result meaning of "success" depends on the HTTP method:
|
||||||
|
* - `GET`: The resource has been fetched and transmitted in the message body.
|
||||||
|
* - `HEAD`: The representation headers are included in the response without any message body.
|
||||||
|
* - `PUT` or `POST`: The resource describing the result of the action is transmitted in the message body.
|
||||||
|
* - `TRACE`: The message body contains the request message as received by the server.
|
||||||
|
*/
|
||||||
|
OK = 200,
|
||||||
|
/**
|
||||||
|
* The request succeeded, and a new resource was created as a result. This is typically the response sent after `POST` requests, or some `PUT` requests.
|
||||||
|
*/
|
||||||
|
CREATED,
|
||||||
|
/**
|
||||||
|
* The request has been received but not yet acted upon. It is noncommittal, since there is no way in HTTP to later send an asynchronous response indicating the outcome of the request. It is intended for cases where another process or server handles the request, or for batch processing.
|
||||||
|
*/
|
||||||
|
ACCEPTED,
|
||||||
|
/**
|
||||||
|
* This response code means the returned metadata is not exactly the same as is available from the origin server, but is collected from a local or a third-party copy. This is mostly used for mirrors or backups of another resource. Except for that specific case, the `200 OK` response is preferred to this status.
|
||||||
|
*/
|
||||||
|
NON_AUTHORITATIVE_INFORMATION,
|
||||||
|
/**
|
||||||
|
* There is no content to send for this request, but the headers may be useful. The user agent may update its cached headers for this resource with the new ones.
|
||||||
|
*/
|
||||||
|
NO_CONTENT,
|
||||||
|
/**
|
||||||
|
* Tells the user agent to reset the document which sent this request.
|
||||||
|
*/
|
||||||
|
RESET_CONTENT,
|
||||||
|
/**
|
||||||
|
* This response code is used when the Range header is sent from the client to request only part of a resource.
|
||||||
|
*/
|
||||||
|
PARTIAL_CONTENT,
|
||||||
|
/**
|
||||||
|
* Conveys information about multiple resources, for situations where multiple status codes might be appropriate.
|
||||||
|
*/
|
||||||
|
MULTI_STATUS,
|
||||||
|
/**
|
||||||
|
* Used inside a `<dav:propstat>` response element to avoid repeatedly enumerating the internal members of multiple bindings to the same collection.
|
||||||
|
*/
|
||||||
|
ALREADY_REPORTED,
|
||||||
|
/**
|
||||||
|
* The server has fulfilled a `GET` request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance.
|
||||||
|
*/
|
||||||
|
IM_USED = 226,
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 3xx Requests *
|
||||||
|
****************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request has more than one possible response. The user agent or user should choose one of them. (There is no standardized way of choosing one of the responses, but HTML links to the possibilities are recommended so the user can pick.)
|
||||||
|
*/
|
||||||
|
MULTIPLE_CHOICES = 300,
|
||||||
|
/**
|
||||||
|
* The URL of the requested resource has been changed permanently. The new URL is given in the response.
|
||||||
|
*/
|
||||||
|
MOVED_PERMANENTLY,
|
||||||
|
/**
|
||||||
|
* This response code means that the URI of requested resource has been changed temporarily. Further changes in the URI might be made in the future. Therefore, this same URI should be used by the client in future requests.
|
||||||
|
*/
|
||||||
|
FOUND,
|
||||||
|
/**
|
||||||
|
* The server sent this response to direct the client to get the requested resource at another URI with a GET request.
|
||||||
|
*/
|
||||||
|
SEE_OTHER,
|
||||||
|
/**
|
||||||
|
* This is used for caching purposes. It tells the client that the response has not been modified, so the client can continue to use the same cached version of the response.
|
||||||
|
*/
|
||||||
|
NOT_MODIFIED,
|
||||||
|
/**
|
||||||
|
* Defined in a previous version of the HTTP specification to indicate that a requested response must be accessed by a proxy. It has been deprecated due to security concerns regarding in-band configuration of a proxy.
|
||||||
|
*/
|
||||||
|
USE_PROXY,
|
||||||
|
/**
|
||||||
|
* This response code is no longer used; it is just reserved. It was used in a previous version of the HTTP/1.1 specification.
|
||||||
|
*/
|
||||||
|
// UNUSED
|
||||||
|
/**
|
||||||
|
* The server sends this response to direct the client to get the requested resource at another URI with the same method that was used in the prior request. This has the same semantics as the `302 Found` HTTP response code, with the exception that the user agent must not change the HTTP method used: if a `POST` was used in the first request, a `POST` must be used in the second request.
|
||||||
|
*/
|
||||||
|
TEMPORARY_REDIRECT = 307,
|
||||||
|
/**
|
||||||
|
* This means that the resource is now permanently located at another URI, specified by the `Location:` HTTP Response header. This has the same semantics as the `301 Moved Permanently` HTTP response code, with the exception that the user agent must not change the HTTP method used: if a `POST` was used in the first request, a `POST` must be used in the second request.
|
||||||
|
*/
|
||||||
|
PERMANENT_REDIRECT,
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 4xx Requests *
|
||||||
|
****************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
|
||||||
|
*/
|
||||||
|
BAD_REQUEST = 400,
|
||||||
|
/**
|
||||||
|
* Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
|
||||||
|
*/
|
||||||
|
UNAUTHORIZED,
|
||||||
|
/**
|
||||||
|
* This response code is reserved for future use. The initial aim for creating this code was using it for digital payment systems, however this status code is used very rarely and no standard convention exists.
|
||||||
|
*/
|
||||||
|
PAYMENT_REQUIRED,
|
||||||
|
/**
|
||||||
|
* The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike `401 Unauthorized`, the client's identity is known to the server.
|
||||||
|
*/
|
||||||
|
FORBIDDEN,
|
||||||
|
/**
|
||||||
|
* The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of `403 Forbidden` to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
|
||||||
|
*/
|
||||||
|
NOT_FOUND,
|
||||||
|
/**
|
||||||
|
* The request method is known by the server but is not supported by the target resource. For example, an API may not allow calling `DELETE` to remove a resource.
|
||||||
|
*/
|
||||||
|
METHOD_NOT_ALLOWED,
|
||||||
|
/**
|
||||||
|
* This response is sent when the web server, after performing server-driven content negotiation, doesn't find any content that conforms to the criteria given by the user agent.
|
||||||
|
*/
|
||||||
|
NOT_ACCEPTABLE,
|
||||||
|
/**
|
||||||
|
* This is similar to `401 Unauthorized` but authentication is needed to be done by a proxy.
|
||||||
|
*/
|
||||||
|
PROXY_AUTHENTIFICATION_REQUIRED,
|
||||||
|
/**
|
||||||
|
* This response is sent on an idle connection by some servers, even without any previous request by the client. It means that the server would like to shut down this unused connection. This response is used much more since some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection mechanisms to speed up surfing. Also note that some servers merely shut down the connection without sending this message.
|
||||||
|
*/
|
||||||
|
REQUEST_TIMEOUT,
|
||||||
|
/**
|
||||||
|
* This response is sent when a request conflicts with the current state of the server.
|
||||||
|
*/
|
||||||
|
CONFLICT,
|
||||||
|
/**
|
||||||
|
* This response is sent when the requested content has been permanently deleted from server, with no forwarding address. Clients are expected to remove their caches and links to the resource. The HTTP specification intends this status code to be used for "limited-time, promotional services". APIs should not feel compelled to indicate resources that have been deleted with this status code.
|
||||||
|
*/
|
||||||
|
GONE,
|
||||||
|
/**
|
||||||
|
* Server rejected the request because the `Content-Length` header field is not defined and the server requires it.
|
||||||
|
*/
|
||||||
|
LENGTH_REQUIRED,
|
||||||
|
/**
|
||||||
|
* The client has indicated preconditions in its headers which the server does not meet.
|
||||||
|
*/
|
||||||
|
PRECONDITION_FAILED,
|
||||||
|
/**
|
||||||
|
* Request entity is larger than limits defined by server. The server might close the connection or return an `Retry-After` header field.
|
||||||
|
*/
|
||||||
|
PAYLOAD_TOO_LARGE,
|
||||||
|
/**
|
||||||
|
* The URI requested by the client is longer than the server is willing to interpret.
|
||||||
|
*/
|
||||||
|
URI_TOO_LONG,
|
||||||
|
/**
|
||||||
|
* The media format of the requested data is not supported by the server, so the server is rejecting the request.
|
||||||
|
*/
|
||||||
|
UNSUPPORTED_MEDIA_TYPE,
|
||||||
|
/**
|
||||||
|
* The range specified by the `Range` header field in the request cannot be fulfilled. It's possible that the range is outside the size of the target URI's data.
|
||||||
|
*/
|
||||||
|
RANGE_NOT_SATISFIABLE,
|
||||||
|
/**
|
||||||
|
* This response code means the expectation indicated by the `Expect` request header field cannot be met by the server.
|
||||||
|
*/
|
||||||
|
EXPECTATION_FAILED,
|
||||||
|
/**
|
||||||
|
* The server refuses the attempt to brew coffee with a teapot.
|
||||||
|
*/
|
||||||
|
IM_A_TEAPOT,
|
||||||
|
/**
|
||||||
|
* The request was directed at a server that is not able to produce a response. This can be sent by a server that is not configured to produce responses for the combination of scheme and authority that are included in the request URI.
|
||||||
|
*/
|
||||||
|
MIDIRECTED_REQUEST = 421,
|
||||||
|
/**
|
||||||
|
* The request was well-formed but was unable to be followed due to semantic errors.
|
||||||
|
*/
|
||||||
|
UNPROCESSABLE_CONTENT,
|
||||||
|
/**
|
||||||
|
* The resource that is being accessed is locked.
|
||||||
|
*/
|
||||||
|
LOCKED,
|
||||||
|
/**
|
||||||
|
* The request failed due to failure of a previous request.
|
||||||
|
*/
|
||||||
|
FAILED_DEPENDENCY,
|
||||||
|
/**
|
||||||
|
* Indicates that the server is unwilling to risk processing a request that might be replayed.
|
||||||
|
*/
|
||||||
|
TOO_EARLY,
|
||||||
|
/**
|
||||||
|
* The server refuses to perform the request using the current protocol but might be willing to do so after the client upgrades to a different protocol. The server sends an `Upgrade` header in a 426 response to indicate the required protocol(s).
|
||||||
|
*/
|
||||||
|
UPGRADE_REQUIRED,
|
||||||
|
/**
|
||||||
|
* The origin server requires the request to be conditional. This response is intended to prevent the 'lost update' problem, where a client `GET`s a resource's state, modifies it and `PUT`s it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict.
|
||||||
|
*/
|
||||||
|
PRECONDITION_REQUIRED = 428,
|
||||||
|
/**
|
||||||
|
* The user has sent too many requests in a given amount of time ("rate limiting").
|
||||||
|
*/
|
||||||
|
TOO_MANY_REQUESTS,
|
||||||
|
/**
|
||||||
|
* The server is unwilling to process the request because its header fields are too large. The request may be resubmitted after reducing the size of the request header fields.
|
||||||
|
*/
|
||||||
|
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
|
||||||
|
/**
|
||||||
|
* The user agent requested a resource that cannot legally be provided, such as a web page censored by a government.
|
||||||
|
*/
|
||||||
|
UNAVAILABLE_OR_LEGAL_REASONS = 451,
|
||||||
|
|
||||||
|
/****************
|
||||||
|
* 5xx Requests *
|
||||||
|
****************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server has encountered a situation it does not know how to handle.
|
||||||
|
*/
|
||||||
|
INTERNAL_SERVER_ERROR = 500,
|
||||||
|
/**
|
||||||
|
* The request method is not supported by the server and cannot be handled. The only methods that servers are required to support (and therefore that must not return this code) are `GET` and `HEAD`.
|
||||||
|
*/
|
||||||
|
NOT_IMPLEMENTED,
|
||||||
|
/**
|
||||||
|
* This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response.
|
||||||
|
*/
|
||||||
|
BAD_GATEWAY,
|
||||||
|
/**
|
||||||
|
* The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This response should be used for temporary conditions and the `Retry-After` HTTP header should, if possible, contain the estimated time before the recovery of the service. The webmaster must also take care about the caching-related headers that are sent along with this response, as these temporary condition responses should usually not be cached.
|
||||||
|
*/
|
||||||
|
SERVICE_UNAVAILABLE,
|
||||||
|
/**
|
||||||
|
* This error response is given when the server is acting as a gateway and cannot get a response in time.
|
||||||
|
*/
|
||||||
|
GATEWAY_TIMEOUT,
|
||||||
|
/**
|
||||||
|
* The HTTP version used in the request is not supported by the server.
|
||||||
|
*/
|
||||||
|
HTTP_VERSION_NOT_SUPPORTED,
|
||||||
|
/**
|
||||||
|
* The server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.
|
||||||
|
*/
|
||||||
|
VARIANT_ALSO_NEGOTIATES,
|
||||||
|
/**
|
||||||
|
* The method could not be performed on the resource because the server is unable to store the representation needed to successfully complete the request.
|
||||||
|
*/
|
||||||
|
INSUFFICIENT_STORAGE,
|
||||||
|
/**
|
||||||
|
* The server detected an infinite loop while processing the request.
|
||||||
|
*/
|
||||||
|
LOOP_DETECTED,
|
||||||
|
/**
|
||||||
|
* Further extensions to the request are required for the server to fulfill it.
|
||||||
|
*/
|
||||||
|
NOT_EXTENDED = 510,
|
||||||
|
/**
|
||||||
|
* Indicates that the client needs to authenticate to gain network access.
|
||||||
|
*/
|
||||||
|
NETWORK_AUTHENTIFICATION_REQUIRED,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatusCode
|
3
src/libs/README.md
Normal file
3
src/libs/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Libs
|
||||||
|
|
||||||
|
Globally independent objects/classes/functions that SHOULD be unit testable by themselve
|
66
src/libs/RFCs/RFC7807.ts
Normal file
66
src/libs/RFCs/RFC7807.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import ResponseBuilder from '../ResponseBuilder'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add headers:
|
||||||
|
* Content-Type: application/problem+json
|
||||||
|
*
|
||||||
|
* following https://www.rfc-editor.org/rfc/rfc7807.html
|
||||||
|
*/
|
||||||
|
export default interface RFC7807 {
|
||||||
|
/**
|
||||||
|
* A URI reference [RFC3986] that identifies the
|
||||||
|
* problem type.
|
||||||
|
*
|
||||||
|
* This specification encourages that, when
|
||||||
|
* dereferenced, it provide human-readable documentation for the
|
||||||
|
* problem type (e.g., using HTML [W3C.REC-html5-20141028]).
|
||||||
|
*
|
||||||
|
* When
|
||||||
|
* this member is not present, its value is assumed to be
|
||||||
|
* "about:blank"
|
||||||
|
*/
|
||||||
|
type?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A short, human-readable summary of the problem
|
||||||
|
* type.
|
||||||
|
*
|
||||||
|
* It SHOULD NOT change from occurrence to occurrence of the
|
||||||
|
* problem, except for purposes of localization (e.g., using
|
||||||
|
* proactive content negotiation; see [RFC7231], Section 3.4).
|
||||||
|
*/
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP status code ([RFC7231], Section 6)
|
||||||
|
* generated by the origin server for this occurrence of the problem.
|
||||||
|
*/
|
||||||
|
status?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A human-readable explanation specific to this
|
||||||
|
* occurrence of the problem.
|
||||||
|
*/
|
||||||
|
details?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A URI reference that identifies the specific
|
||||||
|
* occurrence of the problem.
|
||||||
|
*
|
||||||
|
* It may or may not yield further
|
||||||
|
* information if dereferenced.
|
||||||
|
*/
|
||||||
|
instance?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param error the error (base items are type, status, title details and instance)
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function buildRFC7807(error: RFC7807 & Record<string, any>, response: ResponseBuilder = new ResponseBuilder()): Response {
|
||||||
|
response.addHeader('Content-Type', 'application/problem+json')
|
||||||
|
.body(JSON.stringify(error))
|
||||||
|
.status(error.status ?? 500)
|
||||||
|
return response.build()
|
||||||
|
}
|
67
src/libs/ResponseBuilder.ts
Normal file
67
src/libs/ResponseBuilder.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { objectLoop } from '@dzeio/object-util'
|
||||||
|
import StatusCode from './HTTP/StatusCode'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple builde to create a new Response object
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
public body(body: string | Buffer | object | null | undefined) {
|
||||||
|
if (typeof body === 'object' && !(body instanceof Buffer)) {
|
||||||
|
this._body = JSON.stringify(body)
|
||||||
|
this.addHeader('Content-Type', 'application/json')
|
||||||
|
} else if (body instanceof Buffer) {
|
||||||
|
this._body = body.toString()
|
||||||
|
} else {
|
||||||
|
this._body = body
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private _headers: Record<string, string> = {}
|
||||||
|
public headers(headers: Record<string, string>) {
|
||||||
|
this._headers = headers
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public addHeader(key: string, value: string) {
|
||||||
|
this._headers[key] = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public addHeaders(headers: Record<string, string>) {
|
||||||
|
objectLoop(headers, (value, key) => {
|
||||||
|
this.addHeader(key, value)
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeHeader(key: string) {
|
||||||
|
delete this._headers[key]
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private _status?: number
|
||||||
|
public status(status: StatusCode | number) {
|
||||||
|
this._status = status
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): Response {
|
||||||
|
const init: ResponseInit = {
|
||||||
|
headers: this._headers
|
||||||
|
}
|
||||||
|
if (this._status) {
|
||||||
|
init.status = this._status
|
||||||
|
}
|
||||||
|
return new Response(this._body, init)
|
||||||
|
}
|
||||||
|
}
|
9
src/middleware/README.md
Normal file
9
src/middleware/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Middlewares
|
||||||
|
|
||||||
|
This folder contains middlewares for the SSR pages/endpoints
|
||||||
|
|
||||||
|
They are run for every paths independent of the middleware and in the specified order of the `index.ts`
|
||||||
|
|
||||||
|
## locals
|
||||||
|
|
||||||
|
You can pass variables to other middlewares and endpoints by adding a variable in `locals` and in `App.Locals` in `env.d.ts`
|
5
src/middleware/index.ts
Normal file
5
src/middleware/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { sequence } from "astro/middleware"
|
||||||
|
|
||||||
|
import logger from './logger'
|
||||||
|
|
||||||
|
export const onRequest = sequence(logger)
|
53
src/middleware/logger.ts
Normal file
53
src/middleware/logger.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { defineMiddleware } from "astro/middleware"
|
||||||
|
import ResponseBuilder from 'libs/ResponseBuilder'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Middleware that handle the logging of requests and handling processing errors
|
||||||
|
*/
|
||||||
|
export default defineMiddleware(async ({ request, url }, next) => {
|
||||||
|
const now = new Date()
|
||||||
|
// Date of request User-Agent 32 first chars request Method
|
||||||
|
let prefix = `\x1b[2m${now.toISOString()}\x1b[22m ${request.headers.get('user-agent')?.slice(0, 32).padEnd(32)} ${request.method.padEnd(7)}`
|
||||||
|
|
||||||
|
const fullURL = url.toString()
|
||||||
|
const path = fullURL.slice(fullURL.indexOf(url.pathname, fullURL.indexOf(url.host)))
|
||||||
|
|
||||||
|
if (!import.meta.env.PROD) {
|
||||||
|
// time of request
|
||||||
|
prefix = `\x1b[2m${new Date().toLocaleTimeString('fr')}\x1b[22m`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP Status Code Time to run request path of request
|
||||||
|
console.log(`${prefix} ${''.padStart(5, ' ')} ${''.padStart(7, ' ')} ${path}`)
|
||||||
|
|
||||||
|
// Handle if the request die
|
||||||
|
try {
|
||||||
|
const res = await next()
|
||||||
|
|
||||||
|
if (import.meta.env.PROD) {
|
||||||
|
// HTTP Status time to execute path of request
|
||||||
|
console.log(`${prefix} \x1b[34m[${res.status}]\x1b[0m \x1b[2m${(new Date().getTime() - now.getTime()).toFixed(0).padStart(5, ' ')}ms\x1b[22m ${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
} catch (e) {
|
||||||
|
if (import.meta.env.PROD) {
|
||||||
|
// time to execute path of request
|
||||||
|
console.log(`${prefix} \x1b[34m[500]\x1b[0m \x1b[2m${(new Date().getTime() - now.getTime()).toFixed(0).padStart(5, ' ')}ms\x1b[22m ${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a full line dash to not miss it
|
||||||
|
const columns = (process?.stdout?.columns ?? 32) - 7
|
||||||
|
const dashes = ''.padEnd(columns / 2, '-')
|
||||||
|
|
||||||
|
// colorize the lines to make sur to not miss it
|
||||||
|
console.error(`\x1b[91m${dashes} ERROR ${dashes}\x1b[0m`)
|
||||||
|
console.error(e)
|
||||||
|
console.error(`\x1b[91m${dashes} ERROR ${dashes}\x1b[0m`)
|
||||||
|
|
||||||
|
return new ResponseBuilder()
|
||||||
|
.status(500)
|
||||||
|
.body('An error occured while processing your request')
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
})
|
115
src/models/Dao.ts
Normal file
115
src/models/Dao.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* the Dao is the object that connect the Database or source to the application layer
|
||||||
|
*
|
||||||
|
* you MUST call it through the `DaoFactory` file
|
||||||
|
*/
|
||||||
|
export default abstract class Dao<Object extends { id: any } = { id: any }> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* insert a new object into the source
|
||||||
|
*
|
||||||
|
* @param obj the object to create
|
||||||
|
* @returns the object with it's id filled if create or null otherwise
|
||||||
|
*/
|
||||||
|
abstract create(obj: Omit<Object, 'id' | 'created' | 'updated'>): Promise<Object | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* insert a new object into the source
|
||||||
|
*
|
||||||
|
* @param obj the object to create
|
||||||
|
* @returns the object with it's id filled if create or null otherwise
|
||||||
|
*/
|
||||||
|
public insert: Dao<Object>['create'] = (obj: Parameters<Dao<Object>['create']>[0]) => this.create(obj)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find the list of objects having elements from the query
|
||||||
|
*
|
||||||
|
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
|
||||||
|
* @returns an array containing the list of elements that match with the query
|
||||||
|
*/
|
||||||
|
abstract findAll(query?: Partial<Object>): Promise<Array<Object>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find the list of objects having elements from the query
|
||||||
|
*
|
||||||
|
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
|
||||||
|
* @returns an array containing the list of elements that match with the query
|
||||||
|
*/
|
||||||
|
public find: Dao<Object>['findAll'] = (query: Parameters<Dao<Object>['findAll']>[0]) => this.findAll(query)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find an object by it's id
|
||||||
|
*
|
||||||
|
* (shortcut to findOne({id: id}))
|
||||||
|
*
|
||||||
|
* @param id the id of the object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public findById(id: Object['id']): Promise<Object | null> {
|
||||||
|
return this.findOne({id: id} as Partial<Object>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find an object by it's id
|
||||||
|
*
|
||||||
|
* (shortcut to findOne({id: id}))
|
||||||
|
*
|
||||||
|
* @param id the id of the object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public get(id: Object['id']) {
|
||||||
|
return this.findById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find the first element that match `query`
|
||||||
|
*
|
||||||
|
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
|
||||||
|
* @returns the first element matching with the query or null otherwise
|
||||||
|
*/
|
||||||
|
public async findOne(query?: Partial<Object>): Promise<Object | null> {
|
||||||
|
return (await this.findAll(query))[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update the remote reference of the object
|
||||||
|
*
|
||||||
|
* note: it will not try to insert an item (use `upsert` to handle this)
|
||||||
|
*
|
||||||
|
* @param obj the object to update
|
||||||
|
* @returns an object if it was able to update or null otherwise
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
* @param obj the object to update/insert
|
||||||
|
* @returns the object is updated/inserted or null otherwise
|
||||||
|
*/
|
||||||
|
public async upsert(object: Object | Omit<Object, 'id' | 'created' | 'updated'>): Promise<Object | null> {
|
||||||
|
if ('id' in object) {
|
||||||
|
return this.update(object)
|
||||||
|
}
|
||||||
|
return this.insert(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the object
|
||||||
|
* @param obj the object to delete
|
||||||
|
*
|
||||||
|
* @returns if the object was deleted or not (if object is not in db it will return true)
|
||||||
|
*/
|
||||||
|
abstract delete(obj: Object): Promise<boolean>
|
||||||
|
}
|
54
src/models/DaoFactory.ts
Normal file
54
src/models/DaoFactory.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* TODO:
|
||||||
|
* Add to `DaoItem` your model name
|
||||||
|
* Add to the function `initDao` the Dao
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the different Daos that can be initialized
|
||||||
|
*
|
||||||
|
* Touch this interface to define which key is linked to which Dao
|
||||||
|
*/
|
||||||
|
interface DaoItem {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to get any DAO
|
||||||
|
*/
|
||||||
|
export default class DaoFactory {
|
||||||
|
/**
|
||||||
|
* reference of the different Daos for a correct singleton implementation
|
||||||
|
*/
|
||||||
|
private static daos: Partial<DaoItem> = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a a dao by its key
|
||||||
|
*
|
||||||
|
* it will throw an error if no Dao exists linked to the item key
|
||||||
|
*
|
||||||
|
* @param key the dao key to get
|
||||||
|
* @returns the Dao you want as a singleton
|
||||||
|
*/
|
||||||
|
public static get<Key extends keyof DaoItem>(key: Key): DaoItem[Key] {
|
||||||
|
if (!(key in this.daos)) {
|
||||||
|
const dao = this.initDao(key)
|
||||||
|
if (!dao) {
|
||||||
|
throw new Error(`${key} has no valid Dao`)
|
||||||
|
}
|
||||||
|
this.daos[key] = dao as DaoItem[Key]
|
||||||
|
}
|
||||||
|
return this.daos[key] as DaoItem[Key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* init a dao by its key, it does not care if it exists or not
|
||||||
|
*
|
||||||
|
* @param item the element to init
|
||||||
|
* @returns a new initialized dao or undefined if no dao is linked
|
||||||
|
*/
|
||||||
|
private static initDao(item: keyof DaoItem): any | undefined {
|
||||||
|
switch (item) {
|
||||||
|
default: return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/models/README.md
Normal file
26
src/models/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Models
|
||||||
|
|
||||||
|
this folder contains the Application data layer
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Add a {model}/index.ts contianing your `interface`
|
||||||
|
2. Add a {model}/{model}Dao.ts containing your DAO that `extends` from `Dao.ts`
|
||||||
|
3. Add your Dao to the `DaoFactory.ts` file
|
||||||
|
|
||||||
|
|
||||||
|
## **/index.ts
|
||||||
|
|
||||||
|
file containing the definition of the model
|
||||||
|
|
||||||
|
## **/\*Dao.ts
|
||||||
|
|
||||||
|
File containing the implementation of the Dao
|
||||||
|
|
||||||
|
## Dao.ts
|
||||||
|
|
||||||
|
the Dao.ts is the file each `*Dao.ts` extends from allowing to have a simple, quick and easy to comprehend connectivity
|
||||||
|
|
||||||
|
## DaoFactory.ts
|
||||||
|
|
||||||
|
The DaoFactory file is the file in which you will have the only direct reference to each `*Dao` files and will be sent from there to the rest of the applicaiton layer
|
24
src/pages/404.astro
Normal file
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>
|
5
src/pages/_README.md
Normal file
5
src/pages/_README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Content
|
||||||
|
|
||||||
|
Contains raw content for pages.
|
||||||
|
|
||||||
|
Mostly some static pages or blog posts.
|
23
src/pages/api/event.ts
Normal file
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()
|
||||||
|
}
|
10
src/pages/index.astro
Normal file
10
src/pages/index.astro
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import MainLayout from 'layouts/MainLayout.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout title="Dzeio - Website Template">
|
||||||
|
<main class="container flex flex-col justify-center items-center h-screen gap-6">
|
||||||
|
<h1 class="text-8xl text-center">Dzeio Astro Template</h1>
|
||||||
|
<h2 class="text-2xl text-center">Start editing src/pages/index.astro to see your changes!</h2>
|
||||||
|
</main>
|
||||||
|
</MainLayout>
|
13
src/pages/js/script.js.ts
Normal file
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()
|
||||||
|
}
|
27
tailwind.config.cjs
Normal file
27
tailwind.config.cjs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||||
|
const colors = require('tailwindcss/colors')
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||||
|
theme: {
|
||||||
|
fontFamily: {
|
||||||
|
// add your default font below
|
||||||
|
'sans': ['Font Name', ...defaultTheme.fontFamily.sans]
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
// primary color used by the projet, easily swappable
|
||||||
|
primary: colors.amber
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
// default the container to center the content
|
||||||
|
center: true,
|
||||||
|
padding: {
|
||||||
|
// add a default padding to the container
|
||||||
|
DEFAULT: '1rem'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
5
tests/README.md
Normal file
5
tests/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Tests
|
||||||
|
|
||||||
|
Old Unit tests for each elements
|
||||||
|
|
||||||
|
the paths should correspond to the base folder from `src`
|
9
tests/basic.test.ts
Normal file
9
tests/basic.test.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { expect, test } from 'vitest'
|
||||||
|
|
||||||
|
// Edit an assertion and save to see HMR in action
|
||||||
|
|
||||||
|
test('Math.sqrt()', () => {
|
||||||
|
expect(Math.sqrt(4)).toBe(2);
|
||||||
|
expect(Math.sqrt(144)).toBe(12);
|
||||||
|
expect(Math.sqrt(2)).toBe(Math.SQRT2);
|
||||||
|
});
|
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./node_modules/astro/tsconfigs/strictest.json",
|
||||||
|
"exclude": ["cypress"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src"
|
||||||
|
}
|
||||||
|
}
|
13
vitest.config.ts
Normal file
13
vitest.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/// <reference types="vitest" />
|
||||||
|
import { getViteConfig } from 'astro/config'
|
||||||
|
// import { configDefaults } from 'vitest/config'
|
||||||
|
|
||||||
|
export default getViteConfig({
|
||||||
|
test: {
|
||||||
|
include: [
|
||||||
|
'./tests/**.ts'
|
||||||
|
]
|
||||||
|
/* for example, use global to avoid globals imports (describe, test, expect): */
|
||||||
|
// globals: true,
|
||||||
|
},
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user