tag a tag with the text of `test` from the JSON of the API endpoint
+ *
+ * this library is a mix between systems like React/Svelte (which have a lot of frontend handling) and HTMX (which return HTML entirely from the server)
+ *
+ * _Hyperion is a mix of HyperText(HTML) and Action_
+ *
+ * @author Florian Bouillon
+ * @version 1.0.0
+ * @license MIT
+ */
+export default class Hyperion {
+ private static instance: Hyperion
+ private constructor() {
+ this.init()
+ }
+
+ /**
+ * setup the library for usage
+ */
+ public static setup() {
+ if (!this.instance) {
+ this.instance = new Hyperion()
+ }
+ return this.instance
+ }
+
+ /**
+ * Allows you to manually trigger an element
+ * @param el the element to trigger
+ */
+ public static trigger(el: HTMLElement) {
+ el.dispatchEvent(new Event('hyperion:trigger'))
+ }
+
+ /**
+ * initialise hyperion
+ * @param base the base to query from
+ */
+ private init(base?: HTMLElement) {
+ (base ?? document).querySelectorAll('[data-action]').forEach((it) => {
+ this.initItem(it)
+ })
+ }
+
+ /**
+ * initialize a single element
+ * @param it the element to initialize
+ */
+ private initItem(it: HTMLElement) {
+ // get the trigger action
+ let trigger = it.dataset.trigger ?? 'click'
+
+ // the triggering options
+ const options: {
+ once?: boolean
+ load?: boolean
+ after?: number
+ } = {}
+
+ // handle options
+ if (trigger.includes(' ')) {
+ const splitted = trigger.split(' ')
+ trigger = splitted.shift()!
+ for (const item of splitted) {
+ // item runs only once
+ if (item === 'once') {
+ options.once = true
+ continue
+ }
+ if (item === 'load') {
+ options.load = true
+ continue
+ }
+ if (item.includes(':')) {
+ const [key, value] = item.split(':', 2)
+ // trigger is done after {x}ms (if another trigger is not done)
+ if (key === 'after') {
+ options.after = parseInt(value!)
+ }
+ }
+ }
+ }
+
+ let timeout: NodeJS.Timeout | undefined
+
+ // the triggering function
+ let fn = () => {
+ if (options.after) { // handle options:after
+ if (timeout) {
+ clearTimeout(timeout)
+ timeout = undefined
+ }
+ timeout = setTimeout(() => {
+ this.processElement(it)
+ timeout = undefined
+ }, options.after)
+ } else {
+ this.processElement(it)
+ }
+
+
+ // handle event only happening once
+ if (options.once) {
+ it.removeEventListener('hyperion:trigger', fn)
+ it.removeEventListener(trigger, fn)
+ }
+ }
+
+ // event can also manually be triggered using `hyperion:trigger`
+ it.addEventListener('hyperion:trigger', fn)
+ it.addEventListener(trigger, fn)
+
+ // on load run instantly
+ if (options.load) {
+ this.processElement(it)
+ }
+ }
+
+ /**
+ * process an element that has it's trigger triggered
+ * @param it the element to process
+ */
+ private async processElement(it: HTMLElement) {
+ const action = it.dataset.action // get the URL to try
+ const method = it.dataset.method ?? 'GET' // get the method to use
+ const subPath = it.dataset.path // get a subPath if desired (dot-separated values)
+ let target = it.dataset.target ?? 'innerHTML' // indicate how the data is set
+ const templateQuery = it.dataset.template // get the template query
+ const multiple = it.hasAttribute('data-multiple') // get if the result should be an array
+ const params: Record = {} // the request parameters
+
+ /**
+ * PRE REQUEST HANDLING
+ */
+
+ // handle mendatory elements that are not necessary set
+ if (!templateQuery || !action) {
+ it.dispatchEvent(new CustomEvent('hyperion:attributeError', {
+ detail: {
+ reason: 'data-template or data-action not found'
+ }
+ }))
+ throw new Error(`the data-template (${templateQuery}) or data-action (${action}) attribute is not set :(`)
+ }
+
+ const url = new URL(action, window.location.href)
+
+ // query the remote template
+ const template = document.querySelector(templateQuery)
+ if (!template) {
+ it.dispatchEvent(new CustomEvent('hyperion:templateError', {
+ detail: {
+ reason: 'template not found'
+ }
+ }))
+ throw new Error(`the Template for the query ${templateQuery} was not found ! :(`)
+ }
+
+ // pre-process the target attribute
+ let targetEl: HTMLElement | null = it
+
+ // handle if the target has a querySelector needed
+ if (target.includes(' ')) {
+ // split the target and the query
+ let [newTarget, query] = target.split(' ', 2)
+
+ // set the new target
+ target = newTarget!
+
+ // handle if the query start with `this`
+ let base: HTMLElement | Document = document
+ if (query?.startsWith('this')) {
+ query.replace('this', '')
+ base = it
+ }
+
+ // query the targetElement
+ targetEl = base.querySelector(query!)
+ }
+
+ // handle if the target was not found
+ if (!targetEl) {
+ template.dispatchEvent(new CustomEvent('hyperion:targetError', {
+ detail: {
+ type: 'target not found',
+ data: target
+ }
+ }))
+ throw new Error(`target not found ${target}`)
+ }
+
+ if (it.tagName === 'INPUT') {
+ const value = (it as HTMLInputElement).value
+ if (value) {
+ params[(it as HTMLInputElement).name] = (it as HTMLInputElement).value
+ }
+ } else if (it.tagName === 'FORM') {
+ const form = (it as HTMLFormElement)
+ for (const item of form.elements) {
+ const name = (item as HTMLInputElement).name
+ const value = (item as HTMLInputElement).value
+ if (value) {
+ params[name] = value
+ }
+ }
+ }
+
+ /**
+ * REQUEST HANDLING
+ */
+
+ if (method === 'GET' && objectSize(params) > 0) {
+ objectLoop(params, (value, key) => {
+ url.searchParams.set(key, value)
+ })
+ }
+
+ // do the request
+ const res = await fetch(url, {
+ method: method,
+ body: method === 'POST' && objectSize(params) > 0 ? JSON.stringify(params) : null
+ })
+
+ /**
+ * POST REQUEST HANDLING
+ */
+
+ // handle if the request does not succeed
+ if (res.status >= 400) {
+ it.dispatchEvent(new CustomEvent('hyperion:statusError', {
+ detail: {
+ statusCode: res.status
+ }
+ }))
+ throw new Error(`request returned a ${res.status} error code :(`)
+ }
+
+ // transform the response into JSON
+ let json = await res.json()
+
+ // if subPath was set get to the path described
+ if (subPath) {
+ for (const child of subPath.split('.')) {
+ json = json[child]
+ }
+ }
+
+ // create the template clone(s)
+ const clones: Array = []
+
+ // if the query should have multiple clones (data-multiple)
+ if (multiple) {
+ // the data should be an array :D
+ if (!Array.isArray(json)) {
+ it.dispatchEvent(new CustomEvent('hyperion:dataError', {
+ detail: {
+ type: 'data_not_array',
+ data: json
+ }
+ }))
+ throw new Error(`the data inputted should be an array, instead it is an ${typeof json}`)
+ }
+
+ // loop through the array
+ for (const item of json) {
+ // fill the template element with every values
+ clones.push(this.fillTemplate(template, item))
+ }
+ } else {
+ // fill the template element with the data
+ clones.push(this.fillTemplate(template, json))
+ }
+
+ // handle how the clones are placed
+ switch (target) {
+ case 'outerHTML': // the clones replace the targetted element
+ targetEl.replaceWith(...clones)
+ break;
+ case 'innerHTML': // the clones replace the childs of the targetted element
+ // remove the current childs
+ while (targetEl.firstChild) {
+ targetEl.removeChild(targetEl.firstChild)
+ }
+ // add each clones
+ clones.forEach((it) => targetEl.appendChild(it))
+ break
+ default:
+ break;
+ }
+ }
+
+ /**
+ * fill the template from the data given
+ *
+ * @param template the template to fill
+ * @param data the data to filel with
+ * @returns the filled HTMLElement
+ */
+ private fillTemplate(template: HTMLTemplateElement, data: any): HTMLElement {
+ // clone the template
+ const clone = template.content.cloneNode(true) as HTMLElement
+
+ // go through every elements that has a attribute to fill
+ clone.querySelectorAll('[data-attribute]').forEach((it) => {
+ // get the raw attribute
+ const attrRaw = it.dataset.attribute!
+
+ // parse into an array
+ let attrs: Array
+ if (attrRaw.includes(' ')) {
+ attrs = attrRaw.split(' ')
+ } else {
+ attrs = [attrRaw]
+ }
+
+ // loop through each attributes
+ for (let attr of attrs) {
+ // get the display type of the attribute
+ let target = it.dataset.type ?? 'text'
+
+ // detect if it should be set as an attribute instead
+ const setToAttribute = attr.includes(':')
+ if (setToAttribute) {
+ const splitted = attr.split(':', 2)
+ attr = splitted.pop()!
+ target = splitted[0]!
+ }
+
+ // handle processing of string
+ let content: string
+ // handle a string with attribute processing
+ if (attr.includes('{')) {
+ let res = /\{(.*?)\}/g.exec(attr)
+ while (res) {
+ attr = attr.replace(`${res[0]}`, data[res[1]!])
+ res = /\{(.*?)\}/g.exec(attr)
+ }
+ content = attr
+ } else { // handle basic string
+ content = attr === 'this' ? data : data[attr]
+ }
+
+ if (setToAttribute) { // set it as attribute
+ it.setAttribute(target, content)
+ } else {
+ // get the real target function
+ if (target === 'html') {
+ target = 'innerHTML'
+ } else if ( target === 'text') {
+ target = 'innerText'
+ }
+
+ if (!(target in it)) {
+ template.dispatchEvent(new CustomEvent('hyperion:dataError', {
+ detail: {
+ type: 'data-type invalid',
+ data: target
+ }
+ }))
+ throw new Error(`the data-type is not valid (${target}), it should be one of ('innerHTML' | 'innerText' | 'outerHTML' | 'outerText')`)
+ }
+
+ // set it has inner/outer HTML/Text
+ it[target as 'innerHTML' | 'innerText' | 'outerHTML' | 'outerText'] = content
+ }
+ }
+ // idk if necessary but remove the attributes from the final HTML
+ it.removeAttribute('data-attribute')
+ it.removeAttribute('data-type')
+ })
+
+ // setup the clone to work if it contains Hyperion markup
+ this.init(clone)
+
+ return clone
+ }
+}
diff --git a/src/libs/README.md b/src/libs/README.md
index ac4f233..196df73 100644
--- a/src/libs/README.md
+++ b/src/libs/README.md
@@ -1,3 +1,90 @@
# Libs
Globally independent objects/classes/functions that SHOULD be unit testable by themselve
+
+## Hyperion
+
+Hyperion is a library that allows you to make change to the HTML content dynamically depending on the results of a JSON request
+
+it is kinda a mix of React/Svelte and HTMX
+
+it use both `data-` items and ` ` elements
+
+example:
+
+```html
+
+Checkout
+
+
+
+```
+
+### Attributes
+
+**on Item Attributes**
+
+| nom | default | Type | Valeurs possible | Description |
+| ------------- | --------- | ------- | ------------------------------- | -------------------------------------------------------- |
+| data-trigger | click | Texte | Standard Event, load, after:xxx | see Triggers |
+| data-action | | Texte | | the url to fetch |
+| data-method | GET | Texte | GET, POST, PUT, DELETE | the method to use |
+| data-path | | Texte | | the subpath to start from in the template |
+| data-target | innerHTML | Texte | innerHTML, outerHTML | the target element, see Target |
+| data-template | | Texte | CSS Query | the template CSS query |
+| data-multiple | | Boolean | | if set, it will run on an array, else on everything else |
+
+**In template Attributes**
+
+| nom | default | Type | Valeurs possible | Description |
+| -------------- | ------- | ---- | ---------------- | -------------------------------------------- |
+| data-attribute | | Text | | the attribute to get from the fetched object |
+| data-type | text | Text | | text, html, outerHTML, outerText |
+
+### Triggers
+
+To make a trigger you can define a WebStandard HTML Event like `click`, `mouseover`, etc and it will work after it is emitted by the browser.
+
+there is also a special `load` trigger that will run hyperion on the tag after the page has loaded
+
+you can have multiple triggers by adding them all with a space between (ex: `data-trigger="click, blur"`)
+
+there is also modifiers:
+- `once`: the request will run only once and never again after (note: `load` trigger don't count for this)
+- `after:x`: Dedupplicate the request sent to the server by waiting at least `x` millis with not triggering done
+
+
+### Targets
+
+Targets defin where the generated content will be placed.
+
+- `innerHTML`: it will be placed as a child of the current element
+- `outerHTML`: it will replace the current element at the same position (making it runnable once)
+
+You can also as a second argument change le location of the target by usin a CSS query
+
+example:
+```html
+Button
+
+
+```
+
+Finally you can run the query from the current element you can prepend the query by `this`
+example:
+```html
+
+```
diff --git a/src/pages/api/v1/checkout.ts b/src/pages/api/v1/checkout.ts
new file mode 100644
index 0000000..830480b
--- /dev/null
+++ b/src/pages/api/v1/checkout.ts
@@ -0,0 +1,10 @@
+import type { APIRoute } from 'astro'
+import ResponseBuilder from 'libs/ResponseBuilder'
+
+function randomInt(min = 0, max = 10) {
+ return Math.round(Math.random() * max + min)
+}
+
+export const ALL: APIRoute = ({ url }) => {
+ return new ResponseBuilder().body({caca: 'pokemon', list: [url.searchParams.get('filter'), ...Array(randomInt(1, 20)).fill(0).map(() => randomInt(10, 100))]}).build()
+}
diff --git a/src/pages/api/v1/projects/index.ts b/src/pages/api/v1/projects/index.ts
new file mode 100644
index 0000000..a12a4e1
--- /dev/null
+++ b/src/pages/api/v1/projects/index.ts
@@ -0,0 +1,21 @@
+import type { APIRoute } from 'astro'
+import ResponseBuilder from 'libs/ResponseBuilder'
+
+interface Project {
+ name: string
+ id: string
+}
+
+export const GET: APIRoute = ({ url }) => {
+ const nameFilter = url.searchParams.get('name')
+ return new ResponseBuilder().body(([
+ {
+ name: 'Holisané',
+ id: 'HOLIS'
+ },
+ {
+ name: 'Aptatio',
+ id: 'APTA'
+ }
+ ] as Array).filter((it) => !nameFilter || it.name.includes(nameFilter))).build()
+}
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 96ace34..c6d4d1a 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,4 +1,6 @@
---
+import Button from 'components/global/Button.astro'
+import Input from 'components/global/Input.astro'
import MainLayout from 'layouts/MainLayout.astro'
---
@@ -6,5 +8,56 @@ import MainLayout from 'layouts/MainLayout.astro'
Dzeio Astro Template
Start editing src/pages/index.astro to see your changes!
+ Checkout
+
+
+ Results
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+