Initial commit

This commit is contained in:
Florian Bouillon 2024-05-14 11:23:07 +00:00
commit b7c5f5148e
73 changed files with 10954 additions and 0 deletions

17
.dockerignore Normal file
View 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
View 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
View File

@ -0,0 +1 @@
* text=auto eol=lf

35
.github/workflows/build_and_check.yml vendored Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View 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
View File

@ -0,0 +1,8 @@
{
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.includeLanguages": {
"astro": "html"
}
}

53
Dockerfile Normal file
View 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
View 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 ./

1
README.md Normal file
View File

@ -0,0 +1 @@
# Astro Template

72
astro.config.mjs Normal file
View 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
View File

@ -0,0 +1,3 @@
# e2e
Hold End 2 End tests

8
e2e/example.spec.ts Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View 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
View 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
View 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
View 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]

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,4 @@
---
const json = JSON.stringify(Astro.props)
---
<script id="ASTRO_DATA" is:inline type="application/json" set:html={json}></script>

View 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
View File

@ -0,0 +1,3 @@
# Components
Contains big elements that can be reused by themselve

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

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

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

View 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} />
))}

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

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

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

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

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

View 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" /> -->
</>

View 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"
})
}
}

View 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}&nbsp;<a href="https://www.dzeio.com">Dzeio</a>. Tout droits réservé.</p>
</footer>

View 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
View 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
View 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
View 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
View 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
View 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()} />
))}

View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
{
"extends": "./node_modules/astro/tsconfigs/strictest.json",
"exclude": ["cypress"],
"compilerOptions": {
"baseUrl": "src"
}
}

13
vitest.config.ts Normal file
View 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,
},
});