1st commit

This commit is contained in:
2025-07-01 11:09:51 +02:00
commit 2d3e33d643
787 changed files with 185055 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
**/.DS_Store
**/idx/
wwws/*/dist/**
!wwws/*/src/
!wwws/*/src/**
!wwws/itm/
!wwws/itm/**
devices
frenchlocation
nations
pagans
persons
towns
tribes

13
README.md Normal file
View File

@@ -0,0 +1,13 @@
# apxtri Data
Any objects that are not under app management can be modify manualy by many dev
options/
Liste of referentials
wco/
Web component
wwws/*/src/
Any source code to build in wwws/*/dist/

1
options/conf.json Normal file
View File

@@ -0,0 +1 @@
{"name":"options","schema":"apxtri/schema/options.json","lastupdate":0,"lastupdatedata":"2025-05-18T10:14:03.610Z"}

1505
options/itm/country_en.json Normal file

File diff suppressed because it is too large Load Diff

1505
options/itm/country_fr.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
{
"optionid":"dayofweek_en",
"title": "Country Code",
"description": "Country Code and Info",
"commment": "Alpha-2 country code (ISO 3166-2:XX)",
"lastupdatedata": "",
"lst_idx": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],
"itms": {
"Monday": {
"title": "Monday"
},
"Tuesday": {
"title": "Tuesday"
},
"Wednesday": {
"title": "Wednesday"
},
"Thursday": {
"title": "Thursday"
},
"Friday": {
"title": "Friday"
},
"Saturday": {
"title": "Saturday"
},
"Sunday": {
"title": "Sunday"
}
}
}

View File

@@ -0,0 +1,31 @@
{
"optionid":"dayofweek_fr",
"title": "Country Code",
"description": "Country Code and Info",
"commment": "Alpha-2 country code (ISO 3166-2:XX)",
"lastupdatedata": "",
"lst_idx": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],
"itms": {
"Monday": {
"title": "Lundi"
},
"Tuesday": {
"title": "Mardi"
},
"Wednesday": {
"title": "Mercredi"
},
"Thursday": {
"title": "Jeudi"
},
"Friday": {
"title": "Vendredi"
},
"Saturday": {
"title": "Samedi"
},
"Sunday": {
"title": "Dimanche"
}
}
}

View File

@@ -0,0 +1,36 @@
{
"optionid":"profil_en",
"title": "Profil",
"description": "Profil available in apxtri",
"commment": "",
"lastupdatedata": "",
"lst_idx": [
"anonymous",
"pagan",
"mayor",
"druid",
"person"
],
"itms": {
"anonymous": {
"title": "Unidentified user",
"description": "Unknown to the apxtri network"
},
"pagan": {
"title": "User with an apxtri identity",
"description": "This profile can digitally sign on the apxtri blockchain"
},
"mayor": {
"title": "Administrator of a city",
"description": "He manages the rules specific to a tribe grouping, he finances this city and can define its billing terms for welcoming tribes"
},
"druid": {
"title": "Administrator of a tribe",
"description": "He has a private space in a city, to apply and enforce the rules of the tribe."
},
"person": {
"title": "A member of a tribe",
"description": "A person has an account in a tribe with their alias, it allows them to exchange within a tribe according to the rules set by the druid."
}
}
}

View File

@@ -0,0 +1,35 @@
{
"optionid":"profil_fr",
"title": "Profil",
"description": "Profil disponible dans apxtri",
"commment": "",
"lastupdatedata": "",
"lst_idx": [
"anonymous",
"pagan",
"mayor",
"druid",
"person"
],
"itms": {
"anonymous": {
"title": "Utilisateur non identifié",
"description": "Inconnu du réseau apxtri"
},
"pagan": {
"title": "Utilisateur avec une identité apxtri",
"description": "Ce profil peux signer numériquement sur la blockchain apxtri"
},
"mayor": {
"title": "Administrateur d'une ville",
"description": "Il gére les régles propres à un regroupement de tribu, il finance cette ville et peut definir ses modalités de facturation pour accueillir des tribus"
},
"druid": {
"title": "Administrateur d'une tribu",
"description": "Il dispose d'un espace privée dans une ville , pour y appliquer et faire respecter les régles de la tribu."
},
"person": {
"title": "Un membre d'une tribu",
"description": "Une personne dispose d'un compte dans une tribu avec son alias, il permet d'echanger au sein d'une tribu en fonction des régles mises en place par le druid."
} }
}

View File

View File

@@ -0,0 +1,170 @@
var apx = apx || {};
apx.adminskull = {};
apx.adminskull.show = (id) => {
document.getElementById("maincontent").innerHTML = `Contenu du wco ${id}`;
};
apx.adminskull.genereimg = (alias, size = 100) => {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const context = canvas.getContext("2d");
// Couleur de fond basée sur les lettres du nom
const colors = apx.data.tpldata.headnav.colorslist;
const charCodeSum = alias
.split("")
.reduce((acc, char) => acc + char.charCodeAt(0), 0);
const backgroundColor = colors[charCodeSum % colors.length];
context.fillStyle = backgroundColor;
context.fillRect(0, 0, size, size);
// first and last letter in uppercase
const initials =
alias.charAt(0).toUpperCase() +
alias.charAt(alias.length - 1).toUpperCase();
context.font = `${size / 2}px Arial`;
context.fillStyle = "#FFFFFF"; // Couleur du texte
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText(initials, size / 2, size / 2);
return canvas.toDataURL();
};
apx.adminskull.togglesidebarmobile = (eltbtn) => {
//for mobile need to manage hidden case with other button in headnav
sidebar=document.getElementById("sidebar");
sidebar.classList.toggle('w-0');
sidebar.classList.toggle('w-full'); // Affiche la sidebar sur toute la largeur
sidebar.classList.toggle('-translate-x-full');
if (sidebar.classList.contains('w-full')){
sidebar.classList.remove('hidden');
}else{
sidebar.classList.add('hidden');
}
};
apx.adminskull.toggleheadnav = (icon) => {
document
.querySelectorAll(`.dropdown`)
.forEach((el) => el.classList.add("hidden"));
document.querySelector(`.dropdown.${icon}`).classList.remove("hidden");
};
apx.adminskull.globalevent = () => {
// add here any
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
document
.querySelectorAll("#headnav .dropdown")
.forEach((elt) => elt.classList.add("hidden"));
}
});
document.addEventListener("click", (e) => {
//sidebar open close summary details
if (e.target.tagName.toLowerCase() === "summary") {
document.querySelectorAll("#sidebar ul details").forEach((details) => {
if (details !== e.target.parentNode) {
details.removeAttribute("open");
}
});
}
if (!document.getElementById("headnav").contains(e.target)) {
//if click somewhere else than headnav we close all dropdown that are eventually not hidden
document
.querySelectorAll("#headnav .dropdown")
.forEach((elt) => elt.classList.add("hidden"));
}
});
};
apx.adminskull.navigation = () => {
// test si authentification is valid
// genere les menu en fonction du contexte
const sidebar = document.getElementById("sidebar");
sidebar.querySelectorAll("ul").forEach((e) => e.remove());
sidebar.innerHTML =
sidebar.innerHTML +
Mustache.render(
apx.data.tpl.adminskullverticalnav,
apx.data.tpldata.verticalnav
);
const headnav = document.getElementById("headnav");
const datapagan = { alias: apx.data.headers.xalias };
/* for testing */
apx.data.itms.notifications = {
1: {
id: "1",
text: "notif 1",
from: "apxtri script toto.py",
date: "20250101",
},
2: {
id: "2",
text: "notif 2",
from: "apxtri script toto.py",
date: "20250101",
},
3: {
id: "3",
text: "notif 3",
from: "apxtri script toto.py",
date: "20250101",
},
};
if (
apx.data.itms.notifications &&
Object.keys(apx.data.itms.notifications).length > 0
) {
const notifarray = Object.values(apx.data.itms.notifications);
datapagan.numbernotif = notifarray.length;
datapagan.notif = notifarray
.sort((a, b) => b.date.localeCompare(a.date))
.slice(0, 5);
datapagan.notifcolor = "green";
}
if (
apx.data.itms.messages &&
Object.keys(apx.data.itms.messages).length > 0
) {
const msgarray = Object.values(apx.data.itms.messages);
datapagan.numbermsg = msgarray.length;
datapagan.msg = msgarray
.sort((a, b) => b.date.localeCompare(a.date))
.slice(0, 3);
datapagan.msgcolor = "green";
}
datapagan.aliasimg = apx.adminskull.genereimg(datapagan.alias);
headnav.innerHTML = Mustache.render(
apx.data.tpl.adminskullheadnav,
datapagan
);
apx.adminskull.globalevent();
};
apx.adminskull.search=(element)=>{
const input=element.previousElementSibling
if (!input || input.tagName !== 'INPUT'){
console.log("Check your component no input avaiilable close to this button")
return;
}
document.getElementById("searchtitle").innerHTML+=" "+input.value
// analyse search string + - to convert in json search object with common rules to follow to send some action search in an object
const searchjson={searchtxt:input.value}
if (!apx.data.searchfunction || !apx[apx.data.searchfunction]){
console.log("ERROR: your settings is not correct to use search in your project you must define in apxtri the propertie searchfunction with a value and you must have a function in your project apx[apxtri.searchfunction](search) that return search with in search.results=[{thumbnail,titile,description,..}]")
return;
}
const result = apx[apx.data.searchfunction](searchjson);
result.results.forEach(r=>{
if (!r.thumbnail || r.thumbnail=="") {
r.thumbnail=apx.adminskull.genereimg(r.title,200)
}
})
document.getElementById("searchresults").innerHTML=Mustache.render(apx.data.tpl.adminskullresult,result)
document.getElementById("maincontent").classList.add('hidden')
document.getElementById("searchcontent").classList.remove('hidden')
}
apx.readyafterupdate(apx.adminskull.navigation);

View File

@@ -0,0 +1,26 @@
{
"colorslist": [
"#ffc332",
"#fa6a31",
"#2e7fc8",
"#6aa84f",
"#218787",
"#ffd966",
"#fb8c5a",
"#1c5a8a",
"#4a7c3a",
"#1a6d6d",
"#e6b800",
"#d9531e",
"#5fa8d3",
"#8fbf4d",
"#2d9d9d",
"#cca300",
"#ff8c69",
"#1a3d5c",
"#5c7c3a",
"#4db3b3"
],
"notifplus": "En voir plus",
"msgplus": "Voir tous..."
}

View File

@@ -0,0 +1,93 @@
<!-- Menu Button -->
<div class="flex items-center space-x-4">
<button class="text-gray-500 hover:text-gray-700 focus:outline-none" onclick="apx.adminskull.togglesidebarmobile(this)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
</svg>
</button>
<!-- Search Bar -->
<div class="relative">
<input type="text" class="bg-gray-100 rounded-full pl-10 pr-10 py-2 text-sm text-black focus:outline-none" placeholder="Rechercher...">
<!-- Icône de loupe cliquable à droite -->
<button onclick="apx.adminskull.search(this);" class="absolute inset-y-0 right-0 pr-3 flex items-center">
<svg class="h-5 w-5 text-black-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</button>
</div>
</div>
<!-- Right: Icons and Profile -->
<div class="flex items-center space-x-6">
<!-- Notification Icon -->
{{#notifnumber}}
<div class="relative">
<button class="relative text-gray-700 hover:text-gray-900 focus:outline-none" onclick="apx.adminskull.toggleheadnav('notifications')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405C18.79 14.79 19 13.895 19 13V10c0-3.866-3.134-7-7-7S5 6.134 5 10v3c0 .895.21 1.79.595 2.595L4 17h5m6 0v2a2 2 0 11-4 0v-2m4 0H9" />
</svg>
<span class="absolute top-0 right-0 inline-block w-3 h-3 bg-{{notifcolor}}-500 rounded-full text-white text-xs text-center">{{{notifnumber}}}</span>
</button>
<!-- Dropdown Menu -->
<div class="dropdown notifications hidden absolute left-0 mt-2 w-48 bg-white rounded-lg shadow-lg z-50">
<ul class="py-2">
{{#notif}}
<li>
<a href="#" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">{{text}}<br>{{from}} -- {{date}}</a>
</li>
{{/notif}}
<li><a href="#" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">En voir plus...</a></li>
</ul>
</div>
</div>
{{/notifnumber}}
<!-- Message Icon -->
{{#msgnumber}}
<div class="relative">
<button class="text-gray-700 hover:text-gray-900 focus:outline-none" onclick="apx.adminskull.toggleheadnav('messages')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h8m-8 4h8M5 21l3.386-2.258A8.976 8.976 0 0112 19c4.971 0 9-3.806 9-8.5S16.971 2 12 2 3 5.806 3 10.5c0 1.771.651 3.411 1.748 4.742L5 21z" />
</svg>
<span class="absolute top-0 right-0 inline-block w-3 h-3 bg-{{msgcolor}}-500 rounded-full text-white text-xs text-center">{{{msgnumber}}}</span>
</button>
<!-- Dropdown Menu -->
<div class="dropdown messages hidden absolute left-0 mt-2 w-48 bg-white rounded-lg shadow-lg z-50">
<ul class="py-2">
{{#msg}}
<li><a href="#" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">{{{text}}}<br>{{from}} -- {{date}}</a></li>
{{/msg}}
<li><a href="#" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">Voir les discussions</a></li>
</ul>
</div>
</div>
{{/msgnumber}}
<!-- Language Icon -->
<div class="relative">
<button class="text-gray-700 hover:text-gray-900 focus:outline-none" onclick="apx.adminskull.toggleheadnav('language')">
<span class="inline-block text-sm">🇬🇧</span>
</button>
<!-- Dropdown Menu -->
<div class="dropdown language hidden absolute right-0 mt-2 w-13 bg-white rounded-lg shadow-lg z-50">
<ul class="py-2">
<li>
<a href="admindata_en.html" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">
🇬🇧
</a>
</li>
<li>
<a href="admindata_fr.html" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">
🇫🇷
</a>
</li>
</ul>
</div>
</div>
<div class="relative flex flex-col items-center justify-center text-center">
<!-- Profile Avatar -->
<img src="{{{aliasimg}}}" class="rounded-full h-9 w-9" alt="{{alias}}">
<span class="text-sm mt-2">{{alias}}</span>
</div>
</div>

View File

View File

@@ -0,0 +1,16 @@
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{{#results}}
<div class="card bg-base-100 shadow-xl">
<!-- Thumbnail -->
<figure class="w-full h-48 overflow-hidden">
<img src="{{thumbnail}}" alt="{{title}}" class="w-full h-full object-cover" />
</figure>
<!-- Contenu (titre + description) -->
<div class="card-body">
<h2 class="card-title">{{title}}</h2>
<p>{{description}}</p>
</div>
</div>
{{/results}}
</div>

View File

@@ -0,0 +1,68 @@
{
"sidebarmenutop": [
{
"profils": [
"pagans"
],
"title": "Articles",
"onclick": "apx.adminskull.show('articles','news')",
"submenu": [
{
"title": "Locaux",
"onclick": "apx.admindata.articles.show('localnews')"
},
{
"title": "Rediger",
"onclick": "apx.admindata.articles.editor()"
},
{
"title": "Mes articles",
"onclick": "apx.admindata.articles.show('myarticles')"
}
]
},
{
"profils": [
"pagans"
],
"title": "Messages",
"submenu": [
{
"title": "Discussions",
"onclick": "apx.admindata.messages.show('discussion')"
},
{
"title": "Notification",
"onclick": "apx.admindata.messages.show('notification')"
}
]
},
{
"profils": [
"major"
],
"title": "Admin apxtri",
"onclick": "apx.admindata.apxtri.show('dashboard')",
"submenu": [
{
"title": "Towns",
"onclick": "apx.admindata.apxtri.show('Town')"
},
{
"title": "Tribes",
"onclick": "apx.admindata.apxtri.show('Tribes')"
}
]
}
],
"sidebarmenubottom": [
{
"title": "Mon profil",
"onclick": "apx..()"
},
{
"title": "Log Out",
"onclick": "apx.admindata.logout()"
}
]
}

View File

@@ -0,0 +1,25 @@
<!-- Menu Links Section -->
<ul class="flex-1 mt-4 space-y-1 px-2 text-sm overflow-y-auto">
{{#sidebarmenutop}}
<li class="px-4 py-2 hover:bg-gray-700 rounded-md">
<details>
<summary class="cursor-pointer hover:bg-blue-600">{{title}}</summary>
<ul class="pl-4 mt-2 space-y-2">
{{#submenu}}
<li class="px-4 py-2 cursor-pointer hover:bg-gray-600 rounded-md" onclick="{{{onclick}}}">
{{title}}
</li>
{{/submenu}}
</ul>
</details>
</li>
{{/sidebarmenutop}}
</ul>
<!-- Bottom Links Section -->
<ul class="mb-6 px-2 text-sm">
{{#sidebarmenubottom}}
<li class="px-4 py-2 cursor-pointer hover:bg-gray-700 rounded-md" onclick="{{onclick}}">
<span>{{{title}}}</span>
</li>
{{/sidebarmenubottom}}
</ul>

513
wco/apx/apx.js Normal file
View File

@@ -0,0 +1,513 @@
/*eslint no-undef:0*/
/*eslint-env browser*/
"use strict";
var apx = apx || {};
/**************************************************************************
* apx.js manage data to interact with an apxtri instance from a webpage *
* component can be add with name-wco
*************************************************************************
* This code is not minify and target to be understood by a maximum of
* curious people to audit and give any feedback to support@ndda.fr
*
* To audit it in a browser like chrome:
* - open web developpement (F12)
* - Menu Sources: apx.js , see below for more information
* - : apxid.js, this is the authentification module that works as a microservice
*
*
* Main principle:
* - Usage of localStorage to store any usefull data and save it between session
* - All data and template can be download from an apxtri
* get /api/apxtri/wwws/updatelocaldb{ano}/{tribe}/{xapp}/{pagename}/{version}
* ano = empty if authenticated anonymous if not
* tribe = the tribe where tha xapp is strore
* xapp = the folder name in /tribe/xapp/
* pagename = the name of the page without _xx (language)
* version = 0 or the version currently store (version is to manage cach)
*
* ------------------
* ? State management
* ------------------
* html page must have apxtri with at least headers key
*
<script>
const apxtri={
version:0, // 0 mean it is a first visit if you force to 0 then it will reload all app data
pagename:"pagename", // without _lg.html ex:pageadmin
pageauth:"apxid" local page with authentification without _lg,
headers:{
xalias:"anonymous",
xhash:"anonymous",
xtribe:"tribeId",
xapp:"app name",
xlang:"en",
xdays:0,
xuuid:"0",
xtrkversion:1}
}
wco:{}
//specific init data for this webpage
} ;
</script>
* ++++++++
* ? apx.ready(callback) await DOM is ready equivalent of jquery Document.ready()
* +++++++++
* ? apx.readyafterupdate(callback) allow to add function callback that will be load after apx.ready to add a function to execute apx.readyafterupdate(functname) without () after functname
* +++++++++
* ? apx.listendatawco(newpropertie) allow to store data in apx.data.wco and to listen any change to update HTML DOM content <tag data-wco="propertie"></tag> if apx.data.wco.propertie value change then it change every where data-wco=propertie exist innerHTML, textContent, class, ...value
* to add a new propertie to listen just add it apx.data.wco.propertie={innerHTML:"<p>test</p>"} and run apx.listendatawco(propertie)
* +++++++++
* ? apx.notification(selector, data) insert in tag selector an apx return call into data{status:xxx,ref:"Ref",msg:"key",data:{}}, it use apx.data.ref[data.ref].msg as template and render it with data
* +++++++++
* ? apx.save() save apx.data as {xapp} value in localStorage to be able for the same domain to share data
* +++++++++
* ? apx.update() :
* Run at least once after loading a pagename, what it does:
* - url/page.html?k1=v1&k2=v2#h1=v4&h2=v5 => store in apx.pagecontext = { search: {k1:v1,k2:v2}, hash: {h1:v4,h2:v5} };
* - updatelocaldb from api to localstorage name xapp (options / ref / schema / tpl /tpldata) store in {tribe}/wwws/{xapp}.json with key {pagename}
* - run apx.listendatawco() to allow any apx.data.wco variable to update DOM
* - run any function that were store in apx.afterupdate.
* - run apx.lazyload() to load image in background (this is to decrease time loading for search engine)
* ++++++++++
* */
apx.ready = (callback) => {
if (!callback) {
alert(
"You have an unknown callback apx.ready(callback), you need to order your code callback = ()=>{} then apx.ready(callback), boring but js rules ;-)"
);
}
if (document.readyState != "loading") callback();
// modern browsers
else if (document.addEventListener)
document.addEventListener("DOMContentLoaded", callback);
// IE <= 8
else
document.attachEvent("onreadystatechange", function () {
if (document.readyState == "complete") callback();
});
};
apx.readyafterupdate = (callback) => {
if (!apx.afterupdate) apx.afterupdate = [];
apx.afterupdate.push(callback);
};
apx.lazyload = () => {
document.querySelectorAll("img[data-lazysrc]").forEach((e) => {
// in src/page.html: src contain original img and data-lazysrc='true'
// in dist/page.html src is removed img src is in data-lazysrc=newimage.webp or svg
let src = e.getAttribute("src")
? e.getAttribute("src")
: e.getAttribute("data-lazysrc");
if (e.getAttribute("data-trksrckey")) {
src = `/trk/${src}?alias=${apx.data.headers.xalias}&uuid=${
apx.data.headers.xuuid
}&srckey=${e.getAttribute("data-trksrckey")}&version=${
apx.data.headers.xtrkversion
}&consentcookie=${localStorage.getItem("consentcookie")}&lg=${
apx.data.headers.xlang
}`;
}
e.setAttribute("src", src);
console.log("lazyload track:", src);
e.removeAttribute("data-lazysrc");
});
document.querySelectorAll("[data-lazybgsrc]").forEach((e) => {
e.style.backgroundImage = `url(${e.getAttribute("src")})`;
e.removeAttribute("data-lazybgsrc");
});
document.querySelectorAll("a[data-trksrckey]").forEach((e) => {
let urldestin = e.getAttribute("href");
if (
urldestin.substring(0, 4) != "http" &&
urldestin.substring(0, 1) != "/"
) {
urldestin = "/" + urldestin;
}
const hreftrack = `/trk/redirect?alias=${apx.data.headers.xalias}&uuid=${
apx.data.headers.xuuid
}&srckey=${e.getAttribute("data-trksrckey")}&version=${
apx.data.headers.xtrkversion
}&consentcookie=${localStorage.getItem("consentcookie")}&lg=${
apx.data.headers.xlang
}&url=${urldestin}`;
console.log("href track:", hreftrack);
e.setAttribute("href", hreftrack);
e.removeAttribute("data-trksrckey");
});
};
apx.notification = (selector, data, clearbefore) => {
/**
* @selector a text to use querySelector() in document
* @data apxtri return from any request {status:200,ref:"",msg:"key", data }
* if {status,multimsg:[{ref,msg,data}]} then a multi feedback are in
* @clearbefore boolean if true then it remove the previous message
* @return update the dom selctor with the relevant render message in the relevant language
*/
console.log("notification of ", data);
const el = document.querySelector(selector);
if (!el) {
console.log(
`WARNING !!! check apx.notification selector:${selector} does not exist in this page`
);
return false;
}
if (clearbefore) el.innerHTML = "";
const multimsg = data.multimsg ? data.multimsg : [data];
multimsg.forEach((info) => {
if (!apx.data.ref[info.ref]) {
console.log(
`check apx.data.ref, ${info.ref} does not exist in this page, add it `
);
return false;
} else if (!apx.data.ref[info.ref][info.msg]) {
console.log(
`check apx.data.ref.${info.ref} does not contain ${info.msg} update /schema/lg or /model/lg`
);
return false;
}
el.innerHTML +=
" " + Mustache.render(apx.data.ref[info.ref][info.msg], info.data);
if (data.status == 200) {
el.classList.remove("text-red");
el.classList.add("text-green");
} else {
el.classList.add("text-red");
el.classList.remove("text-green");
}
});
};
apx.listendatawco = (newpropertie) => {
// listen any change in apx.wco.newpropertie and update interface with this new value
// < data-wco="propertie of apx.wco"> is updated with content text, html any attribute in this new value
// to init run apx.listendatawco() this is done by apx.update()
// to dynamicaly add a new propertie to listen just run apx.listendatawco(propertietoadd);
// Then manage your data by modifying apx.wco and it will be update anywhere it use in the webpage
//example:
// <img data-wco="logodetoto" src="urltoimage" "alt">
// apx.wco.logodetoto={src:"newurltoimage",alt:"newalt"} then it will change every where data-wco=logodetoto
//
// <p data-wco="claim">Blabla</p>
// apx.wco.claim={html:"newblabla"}
console.log("From apx.data.wco:", apx.data.wco);
if (!apx.wco) apx.wco = {};
console.log(
"wco dynamic into the webpage",
apx.wco,
"no propertie to add:",
!newpropertie
);
if (!apx.data.wco || Object.keys(apx.data.wco).length == 0) return false;
newpropertie = !newpropertie ? Object.keys(apx.data.wco) : [newpropertie];
console.log("listen apx.data.wco properties:", newpropertie);
newpropertie.forEach((p) => {
const actionprop = (val, elt) => {
if (val.innerHTML) elt.innerHTML = val.innerHTML;
if (val.textContent) elt.textContent = val.textContent;
for (const h in ["innerHTML", "textContent"]) {
if (val[h]) elt[h] = val[h];
}
for (const a in ["src", "alt", "placeholder", "class", "href"]) {
if (val[a]) elt.setAttribute(a, val[a]);
}
};
const elements = document.querySelectorAll(`[data-wco='${p}']`);
elements.forEach((e) => actionprop(apx.data.wco[p], e));
//console.log(p, Object.hasOwnProperty(apx.wco));
if (Object.hasOwnProperty(apx.wco)) {
Object.defineProperty(apx.wco, p, {
set: (newv) => {
this[p] = newv;
elements.forEach((e) => actionprop(newv, e));
},
});
}
});
};
apx.wcoobserver = () => {
/**
* wco web component observer if apxtri.wcoobserver==true,
* Observe existing or creation of any element in DOM with <div wco-name="wconame" id="uniqueid" wco-YYY="aa"></div>
* if create or if any wco-YYY value change it runs apx[wconame].loadwco(id,ctx) where ctx={YYY:aa}
* Example:
* <div id="monComponent" wco-name="monComposant" wco-screen="dashboard" wco-theme="dark" wco-ref="123"></div>
* if innerHTML this OR if any wco-YYYY change =>
* run apx.monComposant.loadwco('monComponent',
* {'wco-name': 'monComposant',
* 'wco-screen': 'dashboard',
* 'wco-theme': 'dark',
* 'wco-ref': '123'});
* Used with wco component manage in apx to communicate between autonomous component to reload content if contexte change.
* Typical example of a wco menu that will load another wco content if not exist and will change a wco-screen to change the content of the wco content
*/
console.log("listen wcoobserver");
const processElement = (element) => {
if (element.nodeType === 1 && element.tagName === "DIV") {
if (element.id && element.hasAttribute("wco-name")) {
const ctx = {};
for (const attr of element.attributes) {
if (attr.name.startsWith("wco-")) {
ctx[attr.name.slice(4)] = attr.value;
}
}
const wcoName = element.getAttribute("wco-name");
if (apx[wcoName] && typeof apx[wcoName].loadwco === "function") {
apx[wcoName].loadwco(element.id, ctx);
} else {
console.log(`ERROR: apx.${wcoName}.loadwco() not found`);
}
}
}
// Process children recursively
if (element.children) {
Array.from(element.children).forEach(processElement);
}
};
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
mutation.addedNodes.forEach((node) => {
processElement(node);
});
}
if (
mutation.type === "attributes" &&
mutation.attributeName.startsWith("wco-")
) {
const element = mutation.target;
if (element.id && element.hasAttribute("wco-name")) {
const ctx = {};
for (const attr of element.attributes) {
if (attr.name.startsWith("wco-")) {
ctx[attr.name.slice(4)] = attr.value;
}
}
const wcoName = element.getAttribute("wco-name");
if (apx[wcoName] && typeof apx[wcoName].loadwco === "function") {
apx[wcoName].loadwco(element.id, ctx);
}
}
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true, // <-- active la détection de changements d'attributs
});
// Pour les éléments déjà présents au chargement
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("div[wco-name]").forEach((element) => {
if (element.id) {
const ctx = {};
for (const attr of element.attributes) {
if (attr.name.startsWith("wco-")) {
ctx[attr.name.slice(4)] = attr.value;
}
}
const wcoName = element.getAttribute("wco-name");
console.log(`load observer of wco for ${wcoName} in id=${element.id}`);
if (apx[wcoName] && typeof apx[wcoName].loadwco === "function") {
apx[wcoName].loadwco(element.id, ctx);
}
}
});
});
//load existing wco-name in the html page to initiate the wco process
// it read and write the wco-name that will trig the observer as a change
document.querySelectorAll("div[wco-name]").forEach((e) => {
const wconame = e.getAttribute("wco-name");
console.log("load wco-name:", wconame);
e.setAttribute("wco-name", wconame);
});
};
// State management
apx.save = () => {
localStorage.setItem(apx.data.headers.xapp, JSON.stringify(apx.data));
};
apx.update = async () => {
if (!apxtri) {
console.log(
'Please add to the html page header, this line const apxtri = { headers: { xtrkversion: 1, xtribe: "smatchit", xapp: "pwa", xlang: "fr", xalias: "anonymous", xhash: "anonymous", xdays: 0} ,pagename:"apxid"} '
);
return;
}
//if (apxtri.forcereload){localStorage.setItem("forcereload",true)};
if (document.querySelector("html").getAttribute("lang")) {
apxtri.headers.xlang = document.querySelector("html").getAttribute("lang");
}
//alert(localStorage.getItem(apxtri.headers.xapp))
if (localStorage.getItem(apxtri.headers.xapp)) {
apx.data = JSON.parse(localStorage.getItem(apxtri.headers.xapp));
//update with current pagename and eventualy pageauth
apx.data.pagename = apxtri.pagename;
if (apxtri.pageauth) apx.data.pageauth = apxtri.pageauth;
// check localstorage in line with current webpage
if (
apx.data.headers.xtribe != apxtri.headers.xtribe ||
apx.data.headers.xlang != apxtri.headers.xlang ||
apx.data.headers.xtrkversion != apxtri.headers.xtrkversion
) {
// if an app change of tribe
localStorage.removeItem(apxtri.headers.xapp);
delete apx.data;
}
}
if (!apx.data) {
console.log("init or reinit apx.data");
apx.data = apxtri;
}
apx.pagecontext = { search: {}, hash: {} };
if (window.location.hash != "") {
window.location.hash
.slice(1)
.split("&")
.forEach((kv) => {
const keyval = kv.split("=");
apx.pagecontext.hash[keyval[0]] = keyval[1];
});
}
if (window.location.search != "") {
window.location.search
.slice(1)
.split("&")
.forEach((kv) => {
const keyval = kv.split("=");
apx.pagecontext.hash[keyval[0]] = keyval[1];
});
}
console.log("apx.pagecontext:", apx.pagecontext);
// Set authenticate parameter if in pagecontext and redirect to the requested url
console.log(
apx.pagecontext.hash.xdays,
apx.pagecontext.hash.xprofils,
apx.pagecontext.hash.xtribe,
dayjs(apx.pagecontext.hash.xdays),
dayjs(apx.pagecontext.hash.xdays).diff(dayjs(), "hours") < 25,
apx.pagecontext.hash.xhash
);
if (
apx.pagecontext.hash.xhash &&
apx.pagecontext.hash.xdays &&
apx.pagecontext.hash.xprofils &&
apx.pagecontext.hash.xtribe &&
dayjs(apx.pagecontext.hash.xdays) &&
dayjs(apx.pagecontext.hash.xdays).diff(dayjs(), "hours") < 25
) {
//Means this page is called from an external auth app
let headervalid = true;
const headerkey = [
"xalias",
"xhash",
"xdays",
"xprofils",
"xtribe",
"xlang",
];
headerkey.forEach((h) => {
if (apx.pagecontext.hash[h]) {
apx.data.headers[h] = (h==="xprofils")? apx.pagecontext.hash[h].split(","):apx.pagecontext.hash[h];
} else {
headervalid = false;
}
});
console.log(headervalid, apx.data.headers);
if (headervalid) {
apx.save();
if (apx.pagecontext.hash.url) {
window.location.href = apx.pagecontext.hash.url;
}
} else {
console.log("Your try to access a page failled with ", apx.pagecontext);
}
}
if (
apx.data.allowedprofils &&
!apx.data.allowedprofils.includes("anonymous") &&
apx.data.pagename !== apx.data.pageauth
) {
const profilintersect = apx.data.allowedprofils.filter((x) =>
apx.data.headers.xprofils.includes(x)
);
console.log("profils authorized:", profilintersect);
if (profilintersect.length == 0) {
alert(apx.data.ref.Middlewares.notallowtoaccess);
return false;
}
if (dayjs().valueOf() - apx.data.headers.xdays > 86400000) {
// need to refresh authentification if possible by opening the pageauth with url context
// the pageauth redirect to this current page after authentification, if not then wait credential
document.location.href = `/${apx.data.pageauth}_${apx.data.headers.xlang}.html#url=${apx.data.pagename}_${apx.data.headers.xlang}.html`;
}
}
console.log("authorized to access");
/* à voir si utile redirect to authentification page pageauth with a redirection if authentify to the pagename (check if /src/ then add it)
window.location.href = `${apxtri.pageauth}_${
apxtri.headers.xlang
}.html?url=${window.location.href.includes("/src/") ? "/src/" : ""}${
apxtri.pagename
}_${apxtri.headers.xlang}.html`;
*/
////////////////////////////////////////////
apx.data.version = 0; //this force an update to be removed in production
///////////////////////////////////////////
const ano = apx.data.headers.xalias == "anonymous" ? "anonymous" : "";
const initdb = `/api/apxtri/wwws/updatelocaldb${ano}/${apx.data.headers.xtribe}/${apx.data.headers.xapp}/${apx.data.pagename}/${apx.data.version}`;
let initset = {};
try {
initset = await axios.get(initdb, {
headers: apx.data.headers,
timeout: 2000,
});
} catch (err) {
console.log(err);
initset = { data: { msg: "unavailableAPI" } };
}
console.log("recupe inidb for ", initdb, initset);
if (initset.data.msg == "forbidenaccess") {
alert(apx.data.ref.Middlewares.notallowtoaccess);
return false;
}
if (initset.data.msg == "unavailableAPI") {
console.log("Your api endpoint is down check your hosted server");
//try again in 30 seconds
setTimeout(apx.update, 30000);
}
if (initset.data.msg == "datamodelupdate") {
// mise à jour local
/*if (initset.data.data.wco) {
console.log("WARNING!!, local apxtri.wco was erase by updatelocaldb.wco");
}*/
Object.keys(initset.data.data).forEach((k) => {
if (k != "headers") {
apx.data[k] = initset.data.data[k];
}
});
/* if (apx.data.confpage.wco && !apx.data.wco){
console.log("update apx.data.wco with localdb cause does not exist")
apx.data.wco=apx.data.confpage.wco;
}
*/
console.log("local update done");
apx.save();
}
apx.listendatawco(); // listen any data-wco tag and update it when apxdatawco propertie change
if (apxtri.wcoobserver) apx.wcoobserver();
if (apx.afterupdate) apx.afterupdate.forEach((cb) => cb()); //run all function store in apx.afterupdate in order
apx.lazyload(); //reload image or any media that takes time to load to improve engine search
apx.save(); //store in local the modification
};
apx.ready(apx.update); //2nd param optional=> true mean does not wait same if apx.lock is set

698
wco/apxauth/apxauth.js Normal file
View File

@@ -0,0 +1,698 @@
var apx = apx || {};
apx.apxauth = {};
apx.apxauth.loadwco = async (id, ctx) => {
// check if not authenticate, do nothing cause by default screensignin and wait authentification
// if authenticate, if url xhash then redirect if no url then change wco-link=screenmytribes
// if (dayjs(apx.data.headers.xdays).diff(dayjs(), "hours") >= 24) apx.apxauth.checkisauth();
//load main.mustache of the component
//when wco-xxx change it run this function
console.log(`Load wconame:apxauth apx.apxauth.loadwco with id:${id} and ctx: ${JSON.stringify(ctx)}`);
const tpldataname = `${apx.data.pagename}_${id}_apxauth`;
const apxauthid = document.getElementById(id)
const data = apx.apxauth.getdata(id, ctx);
if (apxauthid.innerHTML.trim() === "") {
apxauthid.innerHTML = Mustache.render(
apx.data.tpl.apxauthmain,
data
);
}
apxauthid.querySelector(`.screenaction`).innerHTML = Mustache.render(
apx.data.tpl[`apxauthscreen${ctx.link}`],
data
);
apxauthid.querySelector(`.msginfo`).innerHTML = "";
};
apx.apxauth.getdata = (id, ctx) => {
const tpldataname = `${apx.data.pagename}_${id}_apxauth`;
const data = JSON.parse(JSON.stringify(apx.data.tpldata[tpldataname]));
data.id = id;
data.xalias = apx.data.headers.xalias;
data.xtribe = apx.data.headers.xtribe;
data.emailssuport = apx.data.appdata.emailsupport;
switch (ctx.link) {
case "logout":
if (!data.profils) data.profils = [];
apx.data.headers.xprofils.forEach((p) => {
if (!["anonymous", "pagans", "persons"].includes(p)) {
data.profils.push(apx.data.options.profil.itms[p].title);
}
});
data.noprofils = data.profils.length == 0;
data.member = apx.data.headers.xprofils.includes("persons");
data.websites = apx.data.appdata.websites;
// get tribes activities
/*["", "https://wall-ants.ndda.fr"];
axios
.get(`/api/apxtri/tribes/activities`, {
headers: apx.data.headers,
})
.then((rep) => {})
.catch((err) => {});
*/
break;
default:
break;
}
console.log("data for tpl:", data);
return data
};
apx.apxauth.redirecturlwithauth = (url, tribe, webapp, newwindow, windowname = '_blank') => {
url = url.replace(/_[a-zA-Z0-9]{2}\.html/, `_${apx.data.headers.xlang}.html`)
url += `?xtribe=${tribe}&xapp=${webapp}&xalias=${apx.data.headers.xalias}`
url += `&xdays=${apx.data.headers.xdays}&xhash=${apx.data.headers.xhash}`
url += `&xprofils=${apx.data.headers.xprofils.join(',')}`
url += `&xtrkversion=${apx.data.headers.xtrkversion}&xuuid=${apx.data.headers.xuuid}`
if (newwindow) {
try {
const newwin = window.open(url, windowname)
if (newwin === null || typeof newwin === 'undefined') {
console.warn("L'ouverture de la fenêtre a été bloquée par un bloqueur de pop-up.");
// Vous pouvez informer l'utilisateur ici qu'il doit désactiver son bloqueur de pop-up
alert("Votre navigateur a bloqué l'ouverture d'un nouvel onglet. Veuillez autoriser les pop-ups pour ce site.");
} else {
// Optionnel: Mettre le focus sur la nouvelle fenêtre/onglet
newwin.focus();
}
return newwin;
} catch (error) {
console.error("Une erreur est survenue lors de l'ouverture de l'onglet :", error);
return null;
}
}
}
/**
* logout
* Clean any private key into memory of this app and in the backend
*/
apx.apxauth.logout = () => {
axios
.get(`/api/apxtri/pagans/logout`, {
headers: apx.data.headers,
})
.then((rep) => {
console.log("logout", rep);
})
.catch((err) => {
console.log("Erreur logout check:", err);
});
apx.data = apxtri;
apx.save();
if (apx.pagecontext.hash.url) {
window.location.href = apx.pagecontext.hash.url;
} else {
location.reload();
}
};
apx.apxauth.setheadersauth = async (
alias,
passphrase,
publickey,
privatekey,
rememberme
) => {
/**
* Set header with relevant authentification data
* @return {status=200 if apx.data.headers and apx.data.auth properly set}
* {status: 406 or 500 in case issue}
*/
//console.log(alias, passphrase, publickey, privatekey);
if (
alias.length < 3 ||
publickey.length < 200 ||
(privatekey && privatekey.lengtht < 200)
) {
return {
status: 406,
ref: "Pagans",
msg: "aliasorprivkeytooshort",
data: {},
};
}
if (!passphrase) passphrase = "";
if (rememberme) {
apx.data.auth = {
alias: alias,
publickey: publickey,
privatekey: privatekey,
passphrase: passphrase,
};
} else if (apx.data.auth) {
delete apx.data.auth;
apx.save();
}
apx.data.headers.xalias = alias;
apx.data.headers.xdays = dayjs().valueOf();
const msg = `${alias}_${apx.data.headers.xdays}`;
//console.log("pvk", privatekey);
try {
apx.data.headers.xhash = await apx.apxauth.clearmsgSignature(
publickey,
privatekey,
passphrase,
msg
);
} catch (err) {
return {
status: 500,
ref: "Middlewares",
msg: "unconsistentpgp",
data: { err: err },
};
}
apx.save();
console.log("xhash set with:", apx.data.headers.xhash);
return { status: 200 };
};
apx.apxauth.authentifyme = async (
id,
alias,
passphrase,
privatekey,
rememberme
) => {
/**
* Set apx.data.auth with pub, priv, passphrase alias that allow authentification
* set headers with xdays (timestamp) and xhash of message: {alias}_{timestamp} generate with pub & priv key
*
* @Param {key} publickeycreate optional when alias does not exist
*/
//console.log(alias, passphrase);
//console.log(privatekey);
//clean previous answer if exist
const idparent=document.getElementById(id).parentElement?.closest('[wco-name]').getAttribute('id')
document.querySelector(`#${id} .msginfo`).innerHTML = "";
if (alias.length < 3 || privatekey.length < 200) {
apx.notification(`#${id} .msginfo`, {
status: 500,
ref: "Pagans",
msg: "aliasorprivkeytooshort",
data: {},
});
return false;
}
console.log(`get /api/apxtri/pagans/alias/${alias}`);
axios
.get(`/api/apxtri/pagans/alias/${alias}`, {
headers: apx.data.headers,
})
.then(async (rep) => {
//console.log(rep.data);
const setheaders = await apx.apxauth.setheadersauth(
alias,
passphrase,
rep.data.data.publickey,
privatekey,
rememberme
);
if (setheaders.status != 200) {
apx.notification(`#${id} .msginfo`, setheaders);
} else {
console.log("SetheadersOK");
console.log(`/api/apxtri/pagans/isauth`);
axios
.get(`/api/apxtri/pagans/isauth`, {
headers: apx.data.headers,
})
.then((rep) => {
// Authenticate then store profils in header
apx.data.headers.xprofils = rep.data.data.xprofils;
apx.save();
// if this page is call with apxid_fr.html?url=httpsxxx then it redirect to this page.
//alert(`${window.location.href.includes("/src/")?"/src/":""}${apx.pagecontext.hash.url}`)
if (apx.pagecontext.hash.url) {
window.location.href = `${apx.pagecontext.hash.url}`;
} else {
//location.reload();
document.getElementById(idparent).setAttribute('wco-link','mytribes');
}
})
.catch((err) => {
console.log("Not authentify:", err);
delete apx.data.auth;
apx.save();
document.getElementById(idparent).setAttribute("wco-link", "signin")
if (err.response) {
apx.notification(`#${id} .msginfo`, err.response.data);
} else if (err.request) {
apx.notification(`#${id} .msginfo`, {
status: 500,
ref: "Middlewares",
msg: "errrequest",
data: { err: err.request.response },
});
}
});
}
})
.catch((err) => {
//console.log(err.response);
//console.log(err.request);
console.log("checkalias:", err);
if (err.response && err.response.data.msg) {
//remove auth if not well created previously
//console.log(err.response.data.msg);
if (err.response.data.msg == "aliasdoesnotexist") {
delete apx.data.auth;
apx.save();
apx.notification(`#${id} .msginfo`, {
status: 404,
ref: "Pagans",
msg: "aliasdoesnotexist",
data: { alias },
});
//document.getElementById("inputaliasauth").value="";
//document.getElementById("inputpassphraseauth").value="";
//document.getElementById("privatekeyauth").value=""
//window.location.reload();
}
apx.notification(`#${id} .msginfo`, err.response.data);
} else {
apx.notification(`#${id} .msginfo`, {
status: 500,
ref: "Middlewares",
msg: "errrequest",
data: { err },
});
}
});
};
apx.apxauth.recoverykey = (id, aliasoremail) => {
if (aliasoremail.length < 3) {
apx.notification(`#${id} .msginfo`, {
status: 406,
ref: "Pagans",
msg: "recoveryemailnotfound",
data: { tribe: apx.data.headers.xtribe, search: aliasoremail },
});
return false;
}
const recodata = { tribe: apx.data.headers.xtribe, search: aliasoremail };
recodata.emailalias = Checkjson.testformat(aliasoremail, "email")
? "email"
: "alias";
document.querySelector(`#${id} .msginfo`).innerHTML = "";
axios
.post(`/api/apxtri/pagans/keyrecovery`, recodata, {
headers: apx.data.headers,
})
.then((rep) => {
rep.data.data.search = aliasoremail;
apx.notification(`#${id} .msginfo`, rep.data, true);
})
.catch((err) => {
//console.log("error:", err);
const dataerr =
err.response && err.response.data
? err.response.data
: { status: 500, ref: "Pagans", msg: "checkconsole", data: {} };
dataerr.data.search = aliasoremail;
apx.notification(`#${id} .msginfo`, dataerr, true);
});
};
apx.apxauth.generateKey = async (alias, passphrase) => {
/**
* @param {string} alias a unique alias that identify an identity
* @param {string} passphrase a string to cipher the publickey (can be empty, less secure but simpler)
* @return {publickey,privatekey} with userIds = [{alias}]
*/
const pgpparam = {
type: "ecc", // Type of the key, defaults to ECC
curve: "curve25519", // ECC curve name, defaults to curve25519
userIDs: [{ alias: alias }], // you can pass multiple user IDs
passphrase: passphrase, // protects the private key
format: "armored", // output key format, defaults to 'armored' (options: 'armored', 'binary' or 'object')
};
const { privateKey, publicKey } = await openpgp.generateKey(pgpparam);
// key start by '-----BEGIN PGP PRIVATE KEY BLOCK ... '
// get liste of alias:pubklickey await axios.get('api/v0/pagans')
// check alias does not exist
return { alias, privatekey: privateKey, publickey: publicKey };
};
apx.apxauth.verifyKeys = async (
publicKeyArmored,
privateKeyArmored,
passphrase
) => {
try {
// Charger la clé publique
const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored });
// Charger la clé privée
const privateKey = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({
armoredKey: privateKeyArmored,
}),
passphrase: passphrase, // Passphrase de la clé privée (si nécessaire)
});
// Créer un message simple à signer
const message = await openpgp.createMessage({ text: "Test message" });
// Signer le message avec la clé privée
const signedMessage = await openpgp.sign({
message: message, // Message à signer
signingKeys: privateKey, // Clé privée pour signer
});
// Vérifier la signature avec la clé publique
const verificationResult = await openpgp.verify({
message: await openpgp.readCleartextMessage({
cleartextMessage: signedMessage,
}),
verificationKeys: publicKey, // Clé publique pour vérifier
});
// Vérifier si la signature est valide
const { verified } = verificationResult.signatures[0];
await verified; // Resolve la promesse
console.log("Les clés correspondent et sont valides !");
return true;
} catch (error) {
console.error("Erreur lors de la vérification des clés : ", error);
return false;
}
};
apx.apxauth.testcreatekey = async (alias, passphrase) => {
const pgpparam = {
type: "ecc", // Type of the key, defaults to ECC
curve: "curve25519", // ECC curve name, defaults to curve25519
userIDs: [{ alias: alias }], // you can pass multiple user IDs
passphrase: passphrase, // protects the private key
format: "armored", // output key format, defaults to 'armored' (options: 'armored', 'binary' or 'object')
};
const { privateKey, publicKey } = await openpgp.generateKey(pgpparam);
console.log(verifyKeys(publicKey, privateKey, passphrase));
};
apx.apxauth.detachedSignature = async (privK, passphrase, message) => {
/**
* @privK {string} a test priv key
* @passphrase {string} used to read privK
* @message {string} message to sign
* @Return a detached Signature of the message
*/
let privatekey;
if (passphrase == "" || passphrase == undefined) {
privatekey = await openpgp.readKey({ armoredKey: privK });
} else {
privatekey = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: privK }),
passphrase,
});
}
//console.log(message);
const msg = await openpgp.createMessage({ text: message });
//console.log(msg);
const sig = await openpgp.sign({
message: msg,
signingKeys: privatekey,
detached: true,
});
return btoa(sig);
};
apx.apxauth.clearmsgSignature = async (pubK, privK, passphrase, message) => {
/**
* @privK {string} a test priv key
* @passphrase {string} used to read privK
* @message {string} message to sign
* @Return an base64 Signature of the message or error
*/
const publickey = await openpgp.readKey({ armoredKey: pubK });
let privatekey;
if (passphrase == "" || passphrase == undefined) {
privatekey = await openpgp.readKey({ armoredKey: privK });
} else {
privatekey = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: privK }),
passphrase,
});
}
const cleartextMessage = await openpgp.sign({
message: await openpgp.createCleartextMessage({ text: message }),
signingKeys: privatekey,
});
console.log(cleartextMessage);
const verificationResult = await openpgp.verify({
message: await openpgp.readCleartextMessage({ cleartextMessage }),
verificationKeys: publickey,
});
const verified = verificationResult.signatures[0];
const validity = await verified.verified;
if (!validity) throw new Error("invalidsignature");
return btoa(cleartextMessage);
};
apx.apxauth.authenticatedetachedSignature = async (
alias,
pubK,
detachedSignature,
message
) => {
/**
* Check that alias (pubkey) signe a message
* @alias {string} alias link to the publickey
* @pubK {string} publiKey text format
* @detachedSignature {string} a detachedsignatured get from apx.apxauth.detachedSignature
* @message {string} the message signed
* @return {boolean} true the message was signed by alias
* false the message was not signed by alias
*/
const publickey = await openpgp.readKey({ armoredKey: pubK });
const msg = await openpgp.createMessage({ text: message });
const signature = await openpgp.readSignature({
armoredSignature: atob(detachedSignature), // parse detached signature
});
const verificationResult = await openpgp.verify({
msg, // Message object
signature,
verificationKeys: publickey,
});
const { verified, keyID } = verificationResult.signatures[0];
try {
await verified; // throws on invalid signature
//console.log("Signed by key id " + keyID.toHex());
return KeyId.toHex().alias == alias;
} catch (e) {
console.log("Signature could not be verified: " + e.message);
return false;
}
};
apx.apxauth.createIdentity = async (
id,
alias,
recoemail,
passphrase = ""
) => {
document.querySelector(`#${id} .msginfo`).innerHTML = ""
const aliasregex = /^[a-z0-9]*$/;
//console.log(aliasregex.test(alias));
if (!(alias && alias.length > 3 && aliasregex.test(alias))) {
apx.notification(
`#${id} .msginfo`,
{
status: "406",
ref: "Pagans",
msg: "invalidalias",
data: {},
},
true
);
return false;
}
if (recoemail.length > 0 && !Checkjson.testformat(recoemail, "email")) {
apx.notification(`#${id} .msginfo`, {
status: 406,
ref: "Pagans",
msg: "invalidemail",
data: {},
});
return false;
}
axios
.get(`/api/apxtri/pagans/alias/${alias}`, {
headers: apx.data.headers,
})
.then((rep) => {
console.log(rep);
apx.notification(
`#${id} .msginfo`,
{
ref: "Pagans",
msg: "aliasexist",
data: { alias },
},
true
);
})
.catch(async (err) => {
console.log("checkalias:", err);
if (err.response && err.response.status == 404) {
// alias does not exist create it is possible
const keys = await apx.apxauth.generateKey(alias, passphrase);
apx.data.tmpauth = { keys, recoemail, passphrase };
//console.log(apx.data.tmpauth);
["publickey", "privatekey"].forEach((k) => {
console.log(`${id} button.signup${k}`);
const btn = document.querySelector(
`#${id} button.signup${k}`
);
btn.addEventListener("click", () => {
const blob = new Blob([keys[k]], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${alias}_${k}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
});
document
.querySelectorAll(
`#${id} .signupalias, #${id} .signupemailrecovery, #${id} .signuppassphrase`
)
.forEach((e) => e.setAttribute("disabled", "disabled"));
document
.querySelector(`#${id} .getmykeys`)
.classList.remove("hidden");
document
.querySelector(`#${id} .btncreatekey`)
.classList.add("hidden");
} else {
apx.notification(
`#${id} .msginfo`,
{
ref: "Middlewares",
msg: "errrequest",
data: {},
},
true
);
}
});
};
/**
*
* @param {string} alias to create
* @param {string} publickey
* @param {string} trustedtribe if none => means no passphrase, no privatekey, no trustedtribe
* @param {string} passphrase
* @param {string} privatekey
* @param {string} email if none => means no passphrase, no privatekey, no trustedtribe
*
* if email!=none and trustedtribe!= none create a person with parson profil in trustedtribe
* if email!=none and trustedtribe==none then send an email at registration with all element but doi not store in backend for futur recovery
*
*/
apx.apxauth.test = () => {
//"apx.apxauth.registerIdentity(document.getElementById('inputalias').value,document.getElementById('publickey').document.getElementById('inputpassphrase').value)"
console.log(apx.data.tmpauth);
};
apx.apxauth.registerIdentity = async (id, trustedtribe) => {
const authid = document.getElementById(id);
// trustedtribe boolean
//previously store in apx.data.tmpauth={keys:{alias,privatekey,publickey},recoemail,passphrase}
const setheaders = await apx.apxauth.setheadersauth(
apx.data.tmpauth.keys.alias,
apx.data.tmpauth.passphrase,
apx.data.tmpauth.keys.publickey,
apx.data.tmpauth.keys.privatekey,
false
);
if (setheaders.status != 200) {
apx.notification(`#${id} .msginfo`, setheaders);
} else {
// add withpublickeyforcreate to check isAuthenticated alias does not already exist
const data = {};
data.alias = apx.data.tmpauth.keys.alias;
data.publickey = apx.data.tmpauth.keys.publickey;
console.log(apx.data.tmpauth.recoemail, Checkjson.testformat(apx.data.tmpauth.recoemail, "email"))
if (apx.data.tmpauth.recoemail && Checkjson.testformat(apx.data.tmpauth.recoemail, "email")) {
data.passphrase = apx.data.tmpauth.keyspassphrase;
data.privatekey = apx.data.tmpauth.keysprivatekey;
data.email = apx.data.tmpauth.recoemail;
}
data.trustedtribe = trustedtribe;
axios
.post(`/api/apxtri/pagans`, data, { headers: apx.data.headers })
.then((reppagan) => {
//console.log(reppagan.data);
apx.notification(`#${id} .msginfo`, reppagan.data);
authid.querySelector(`.btncreateidentity`)
.classList.add("hidden");
authid.querySelector(`.signupbtnreload`)
.classList.remove("hidden");
//remove tmp cause create phc change to keep tplauth in memory and avoid asking again the pasword
//delete apx.data.tmpauth;
//apx.save();
})
.catch((err) => {
console.log("error:", err);
const dataerr =
err.response && err.response.data
? err.response.data
: { status: 500, ref: "Pagans", msg: "", data: {} };
apx.notification(`#${id} .msginfo`, dataerr);
});
}
};
apx.apxauth.jointribe = (id) => {
/**
* Allow a pagan to register as a person into a tribe
* header must be authenticated with alias into an app belonging to xtribe AND schema person must have apxaccessright with role "pagan": {"C": []}
*/
//console.log(apx.data);
if (!apx.data.headers.xprofils.includes("persons")) {
apx.data.headers.xprofils.push("persons");
}
const data = {
alias: apx.data.headers.xalias,
profils: apx.data.headers.xprofils,
};
axios
.put(`/api/apxtri/pagans/person/${apx.data.headers.xtribe}`, data, {
headers: apx.data.headers,
})
.then((rep) => {
apx.notification(`#${id} .msginfo`, rep.data);
axios
.get(`/api/apxtri/pagans/logout`, {
headers: apx.data.headers,
})
.then((rep) => {
console.log("logout", rep);
apx.apxauth.authentifyme(
id,
apx.data.auth.alias,
apx.data.auth.passphrase,
apx.data.auth.privatekey
);
})
.catch((err) => {
console.log("Erreur logout check:", err);
});
})
.catch((err) => {
console.log("sorry", err);
if (err.response && err.response.data)
apx.notification("#msginfo", err.response.data);
else
apx.notification("#msginfo", {
status: 500,
ref: "Pagans",
msg: "errcreate",
data: {},
});
});
};

View File

@@ -0,0 +1,5 @@
{
"aliasinvalid": "Combinaison de 3 à 150 caractères <br /> composée de minuscules (a à z) et/ou de chiffres (0 à 9)",
"aliastitle": "Uniquement minuscules ou chiffres",
"privatekeyplaceholder": "Votre clé privée"
}

View File

@@ -0,0 +1,7 @@
<!-- screen action-->
<div class="screenaction mt-5 sm:mx-auto sm:w-full sm:max-w-sm">
</div>
<!-- feedback action-->
<div class="my-5">
<p class="msginfo text-center text-info"></p>
</div>

View File

@@ -0,0 +1,44 @@
<div class="mt-1">
<label class="input validator mbt-1">
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M11.89 4.111a5.5 5.5 0 1 0 0 7.778.75.75 0 1 1 1.06 1.061A7 7 0 1 1 15 8a2.5 2.5 0 0 1-4.083 1.935A3.5 3.5 0 1 1 11.5 8a1 1 0 0 0 2 0 5.48 5.48 0 0 0-1.61-3.889ZM10 8a2 2 0 1 0-4 0 2 2 0 0 0 4 0Z"></path>
</g>
</svg>
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z"></path>
</g>
</svg>
<input id="inputaliasrecovery" type="text" placeholder="mail@site.com | alias" required />
</label>
<div class="validator-hint hidden">
Enter a valid email or an alias (lowercase a-z and 0-9)
</div>
</div>
<div class="my-5">
<p>
Si vous avez fait confiance à ce domaine pour garder vos clés, un email va être envoyé avec vos clés.
</p>
</div>
<div class="my-5">
<button
class="btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="apx.apxauth.recoverykey('{{id}}',document.getElementById('inputaliasrecovery').value);"
>
M'envoyer un email avec mes clés
</button>
</div>

View File

@@ -0,0 +1,41 @@
<div class="space-y-6 text-justify">
<h2>Qu'est-ce qu'une identité numérique décentralisée?</h2>
<p>
C'est <span class="text-secondary">un moyen de s'identifier en prouvant qu'on est le propriétaire
d'un alias ou d'une clé publique</span>. Cette clé publique est accessible à tous et utilisée dans le
monde numérique pour informer, payer, échanger,... et porte une
réputation publique.
</p>
<p>
Concrètement, c'est une paire de fichiers texte appelée clé publique
et clé privée. La clé publique ne porte pas d'information
personnelle autre que celles que vous avez bien voulu y associer.
</p>
<p>
Une fonction mathématique permet au propriétaire de la clé privée de
signer un message. Le destinataire dispose d'une autre fonction qui
permet de vérifier que la signature a été faite avec la clé privée.
</p>
<p>
Cette interface permet de créer une identité et de l'utiliser pour
s'authentifier pour 24 heures. Elle n'envoie que le couple alias/clé
publique sur internet, la clé privée est
<span class="text-secondary">votre propriété et ne doit jamais être communiquée</span>. Si vous
la perdez, vous ne pourrez plus récupérer les informations
associées. Sauf si vous
<span class="text-secondary">avez fait confiance à ce nom de domaine</span>, vous pourrez lui
demander d'envoyer un email avec ces clés.
</p>
<p>
Vous pouvez avoir autant d'identités que vous voulez, vous pouvez
créer une identité pour des objets uniques. La seule limite est qu'à
partir du moment où vous associez des informations personnelles à
cette clé, le destinataire de ces informations peut les relier aux
activités de cette identité inscrite dans la blockchain apxtri.
</p>
<p>
Pour auditer le code js, utiliser l'outil de développement de votre
navigateur. Pour toute remarque, question ou détection de failles :
{{supportemail}}
</p>
</div>

View File

@@ -0,0 +1,39 @@
<div class="flex flex-col space-y-1 text-center">
<div class="mt-1">
<h1 class="mb-6">
Bonjour {{xalias}},
</h1>
<p>
Si cet appareil ne vous appartiens pas et que vous n'utilisez pas l'application, vous devriez vous deconnecter.
</p>
<p class="text-center text-gray-500">
Nettoyer mes traces de cet appareil?
<a class="font-semibold leading-6 text-secondary hover:text-primary"
onclick="apx.apxauth.logout('{{id}}','logout','logout','apxauth')">Se deconnecter</a>
</p>
</div>
<div class="mt-4">
<p class="text-center text-gray-500">
Voir mes échanges?
<a class="font-semibold leading-6 text-secondary hover:text-primary"
href="https://wall-ants.ndda.fr/apxwallet_fr.html" >Mon activité </a>
</p>
{{#member}}
<p>
Vous êtes membre de {{xtribe}} {{#noprofils}} sand profil particulier {{/noprofils}} {{^noprofils}}avec le(s) profil(s):<br><span class="text-info"> {{#profils}} {{.}}<br> {{/profils}} </span> {{/noprofils}}
</p>
{{/member}}
{{^member}}
<p> Vous n'êtes pas encore membre de {{xtribe}} </p>
<p class=" mt-1 text-center text-gray-500">
Envie d'jouter cette tribut {{xtribe}}?
<a class="font-semibold leading-6 text-secondary hover:text-primary"
onclick="apx.apxauth.jointribe('{{id}}')">Rejoindre {{xtribe}}</a>
</p>
{{/member}}
<p>Les applications ou pages web de {{xtribe}} à visiter:<br>
{{#websites}}<a class="font-semibold leading-6 text-secondary hover:text-primary" href='{{{href}}}'>{{{name}}}</a><br> {{/websites}}
</p>
<button class="btn btn-primary" onclick="apx.apxauth.runtest()">testbtn</button>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<div class="flex flex-col space-y-1 text-center">
<div class="mt-1">
<h1 class="mb-6">
Bonjour {{xalias}},
</h1>
</div>
<div class="mt-4">
<p class="text-center text-gray-500">
Redirige vers
<a class="font-semibold leading-6 text-secondary hover:text-primary"
onclick="apx.apxauth.redirecturlwithauth('http://recruiter.smatchit.newdev.ants/src/offer_fr.html','smatchit','recruiter',true);" >Redirige vers recruiter.smatchit.io/offer_fr.html&xhash....</a>
</p>
{{#member}}
<p>
Vous êtes membre de {{xtribe}} {{#noprofils}} sand profil particulier {{/noprofils}} {{^noprofils}}avec le(s) profil(s):<br><span class="text-info"> {{#profils}} {{.}}<br> {{/profils}} </span> {{/noprofils}}
</p>
{{/member}}
<p>Les applications ou pages web de {{xtribe}} à visiter:<br>
{{#websites}}<a class="font-semibold leading-6 text-secondary hover:text-primary" href='{{{href}}}'>{{{name}}}</a><br> {{/websites}}
</p>
<button class="btn btn-primary" onclick="apx.apxauth.runtest()">testbtn</button>
</div>
</div>

View File

@@ -0,0 +1,69 @@
<p data-wco="createid" class="text-center text-neutral-content">
{{{signintitle}}}
</p>
<div class="mt-2">
<label class="input validator">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z"></path>
</g>
</svg>
<input
class="signinalias"
type="input"
required
placeholder="alias"
pattern="[a-z0-9\-]*"
minlength="3"
maxlength="30"
title="{{{aliastitle}}}"
/>
</label>
<p class="validator-hint hidden"> {{{aliasinvalid}}}</p>
</div>
<div class="mt-2">
<label class="input mt-1">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"></path>
</g>
</svg>
<input type="text" class="signinpassphrase" placeholder="passphrase (option)" />
</label>
</div>
<div class="mt-2">
<textarea rows=5 class="mt-2 textarea signinprivatekey" placeholder="{{{privatekeyplaceholder}}}"></textarea>
</div>
<div class="flex m-6">
<div class="w-14 flex-none">
<input type="checkbox" checked="checked" class="checkbox signinrememberme" />
</div>
<div class="flex-1">
<p class="text-sm text-justify" >{{{remembermetext}}}</p>
</div>
</div>
<div class="m-4">
<button
class="btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="const loginid= document.getElementById('{{id}}');apx.apxauth.authentifyme(
'{{id}}',
loginid.querySelector('.signinalias').value,
loginid.querySelector('.signinpassphrase').value,
loginid.querySelector('.signinprivatekey').value,
loginid.querySelector('.signinrememberme').checked
)">
{{{authentifyme}}}
</button>
</div>

View File

@@ -0,0 +1,121 @@
<p data-wco="createid" class="text-center text-neutral-content">
{{{signuptitle}}}
</p>
<div class="paramid">
<div class="mt-2">
<label class="input validator mbt-1">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z"></path>
</g>
</svg>
<input
class="signupalias"
type="input"
required
placeholder="alias"
pattern="[a-z0-9\-]*"
minlength="3"
maxlength="30"
title="{{{aliastitle}}}"
/>
</label>
<div class="validator-hint hidden">
<p>{{{aliasinvalid}}}</p>
</div>
</div>
<div class="mt-2">
<label class="input validator mt-1">
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg>
<input class="signupemailrecovery" type="email" placeholder="mail@site.com" required />
</label>
<div class="validator-hint hidden">
{{{emailinvalid}}}
</div>
</div>
<div class="mt-2">
<label class="input mt-1">
<svg class="h-[1em] opacity-90" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="1"
fill="black"
stroke="black"
>
<path d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"></path>
</g>
</svg>
<input type="text" class="signuppassphrase" placeholder="passphrase (option)" />
</label>
</div>
<div class="mt-5">
<button
class="btncreatekey btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="const authid=document.getElementById('{{id}}');console.log('{{id}}'); apx.apxauth.createIdentity(
'{{id}}',
authid.querySelector('.signupalias').value,
authid.querySelector('.signupemailrecovery').value,
authid.querySelector('.signuppassphrase').value
)"
>
{{{createkey}}}
</button>
</div>
</div>
<div class="getmykeys hidden mt-1">
<div class="flex m-6">
<div class="w-14 flex-none">
<input type="checkbox" checked="checked" class="signuptrustedcheck checkbox checkbox-secondary" />
</div>
<div class="flex-1">
<p class="text-sm text-justify" >{{{trusttext}}}</p>
</div>
</div>
<div class="downloadkeys text-center mt-1">
<button
class="signuppublickey btn btn-outline btn-accent text-white shadow-sm"
>
{{{downloadPuK}}}
</button>
<button
class="signupprivatekey btn btn-outline btn-accent text-white shadow-sm"
>
{{{downloadPrK}}}
</button>
</div>
<div class="mt-2">
<button
class="btncreateidentity btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="const authid=document.getElementById('{{id}}');apx.apxauth.registerIdentity(
'{{id}}',
authid.querySelector('.signuptrustedcheck').checked
)"
>{{{saveidentity}}}
</button>
<button
class="signupbtnreload hidden btn btn-primary w-full justify-center hover:bg-secondary focus:outline focus:outline-primary"
onclick="location.reload(true)"
>
{{{nextpage}}}
</button>
</div>
</div>

6
wco/chatroom/chatroom.js Normal file
View File

@@ -0,0 +1,6 @@
var apx = apx || {};
apx.chat = {};
apx.chat.show=()=>{
}

1
wco/conf.json Normal file
View File

@@ -0,0 +1 @@
{"name":"wco","schema":"apxtri/schema/wco.json","lastupdate":0}

View File

@@ -0,0 +1,46 @@
<div class="contactform grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<input type="text" id="name" placeholder="Votre nom"
class="input input-bordered bg-white" />
<label for="name" class="label floating-label">Votre nom</label>
</div>
<div class="form-control">
<input type="email" id="mail" placeholder="Votre Email"
class="input input-bordered bg-white" />
<label for="mail" class="label floating-label">Votre Email</label>
</div>
<div class="form-control">
<input type="text" id="mobile" placeholder="Votre téléphone"
class="input input-bordered bg-white" />
<label for="mobile" class="label floating-label">Votre téléphone</label>
</div>
<div class="form-control">
<select class="select select-bordered bg-white" id="service">
<option selected value="syndic">Un syndic</option>
<option value="particulier">Un particulier</option>
<option value="entreprise">Une entreprise</option>
<option value="collectivité">Une collectivité</option>
</select>
<label for="service" class="label floating-label">Vous êtes:</label>
</div>
<div class="form-control md:col-span-2">
<textarea id="message" class="textarea textarea-bordered bg-white h-32"></textarea>
<label for="message" class="label floating-label">Message</label>
</div>
<div class="md:col-span-2 text-center">
<button class="btn btn-primary w-full py-3"
onclick="apx.sendform(...)">Envoyer votre demande</button>
</div>
<!-- Messages de feedback -->
<div class="answerok alert alert-success hidden"></div>
<div class="answerko alert alert-error hidden">
<p class="text-error-content">En maintenance, ré-essayez plus tard</p>
</div>
</div>

View File

@@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "/schema/contact",
"title": "Contact information ",
"description": "A form submission data",
"type": "object",
"properties": {
"contactid": {
"title":"A unique string fromalias_fromuuid_toalias_touuid_timestamp",
"type":"string",
"default":"createcontactid",
"formclass":"hidden"
},
"name":{
"title":"Name",
"description":"",
"type":"string",
"formclass":"input",
"placeholder":"Votre nom"
},
"mailm":{
"title":"Email",
"description":"",
"type":"string",
"formclass":"input",
"placeholder":"Votre nom"
},
"mobile":{
"title":"",
"description":"",
"type":"string",
"formclass":"input"
}
}
}

View File

@@ -0,0 +1,127 @@
var apx = apx || {};
apx.form = {};
/**
* Data form schema
* {}
*
* Will produce a html expected:
* <div class="formmanager">
* <input data-name="keydata" data-format="checkJSON" value="defaultvalue" placeholder="" />
* .. any other data field
* <button class="getdata" onclick="apx.form.submit(this,cb)">save</button>
* </div>
*
*/
apx.form.build = (elt,schema,data)=>{
// Build in html a form with all prerequest and prefill by data into a DOM element elt
};
apx.form.submit = (elt,cb)
/**
* To install in html input onclick=apx.form.enter to simulate a senddata
* @param {*} elt
* @param {*} event
*/
apx.form.enter = (elt, event) => {
if (event.keyCode === 13) {
const getform = elt.closest(".contactform");
event.preventDefault();
getform.querySelector(".sendregister").click();
}
};
// apx.sendform(this,{route:'actions/contact',order:'registercontact',srckey:'teasingwebpage',mlist:'getinform'},fctregisteremail)
registerlist.check = {
email:
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
telephone: /^0[1-9][0-9]{8}$/,
profil: /^(seeker|recruiter)$/,
};
registerlist.msg = {
email: "Vérifier votre email ",
telephone: "Verifier votre téléphone ",
profil: "Choisir votre profil ",
serverissue:
"Désolé, un probléme empeche votre inscription, réessayer plus tard",
register: "Vous êtes bien inscrits",
alreadysent: "Vous avez déjà envoyé votre email ",
};
registerlist.send = async (elt) => {
const formdata = {};
// formdata.uuid = (localStorage.getItem('xuuid')) ? localStorage.getItem('xuuid'):"uuidunknown";
const form = elt.closest(".contactform");
form.querySelector(".answer").innerHTML = "";
var valid = true;
form.querySelectorAll("input,textarea,option:checked").forEach((e) => {
if (!e.name) e.name = info.getAttribute("name");
if (e.name == "email") formdata.typekey = "email";
if (e.name == "telephone") formdata.typekey = "telephone";
if (!e.type || e.type !== "radio" || (e.type == "radio" && e.checked)) {
//check value
if (
registerlist.check[e.name] &&
!registerlist.check[e.name].test(e.value)
) {
form.querySelector(".answer").innerHTML +=
registerlist.msg[e.name] + " ";
form.querySelector(".answer").classList.remove("d-none");
valid = false;
}
if (!formdata[e.name]) {
formdata[e.name] = e.value;
} else {
// array case multi input with same name
formdata[e.name] = [formdata[e.name]];
formdata[e.name].push(e.value);
}
}
});
formdata.tribe=apxtri.headers.xtribe
if (!formdata.profil || formdata.profil == "") {
// Pas de choix
formdata.profil = "both";
//form.querySelector(".answer").innerHTML = registerlist.msg.profil;
//form.querySelector(".answer").classList.remove("d-none");
}
//add phil
formdata.mlist += formdata.profil;
console.log(formdata);
if (
valid &&
formdata.profil &&
formdata.srckey &&
(formdata.email || formdata.telephone)
) {
form.querySelector(".submitbtn").classList.add("d-none");
form.querySelector(".loaderbtn").classList.remove("d-none");
const datasent = await axios.post(
"/api/apxtri/notifications/registeranonymous",
formdata,
{ headers: apx.data.headers }
);
//console.log(datasent)
if (datasent.data.status == 200) {
form.querySelector(".answer").innerHTML = registerlist.msg.register;
form.querySelector(".answer").classList.remove("d-none");
form.querySelector(".loaderbtn").classList.add("d-none");
elt.setAttribute(
"onclick",
`"alert('${registerlist.msg.alreadysent}');"`
);
//document.location.href = "thank-you.html";
} else {
form.querySelector(".answer").innerHTML = registerlist.msg.serverissue;
form.querySelector(".answer").classList.remove("d-none");
}
} else {
console.log(
"Check your form it miss something profil or srckey or email or telephone"
);
}
};

14
wco/itm/admindata.json Normal file
View File

@@ -0,0 +1,14 @@
{
"wconame": "admindata",
"owner": "philc",
"price": 1,
"aliascode": [],
"commentaliascode": "if paid wco then [tribename_uniquecode,...]",
"codehash": "123",
"thumbnail": "",
"title": "Manage admindata page",
"description": "",
"tpl": {},
"tpldata": {},
"ref": {}
}

36
wco/itm/adminskull.json Normal file
View File

@@ -0,0 +1,36 @@
{
"wconame": "adminskull",
"owner": "philc",
"price": 1,
"aliascode": [],
"commentaliascode": "if paid wco then [tribename_uniquecode,...]",
"codehash": "123",
"thumbnail": "",
"title": "Vertical and horinzontal menu",
"description": "",
"lang": [
"fr"
],
"tpl": {
"adminskullverticalnav": "apxtri/objects/wco/adminskull/verticalnav",
"adminskullresult": "apxtri/objects/wco/adminskull/result",
"adminskullmain": "apxtri/objects/wco/adminskull/main",
"adminskullheadnav": "apxtri/objects/wco/adminskull/headnav"
},
"tpldatamodel": {
"headnav":"apxtri/objects/wco/adminskull/headnav",
"verticalnav":"apxtri/objects/wco/asminskull/verticalnav"
},
"options": {
"profil": "{{tribe}}/objects/options/profil"
},
"ref": {
"Odmdb": "apxtri/objects/tplstrings/Odmdb",
"Pagans": "apxtri/objects/tplstrings/Pagans",
"Persons": "apxtri/objects/tplstrings/Persons"
},
"schema": [
"apxtri/objects/pagans",
"{{tribe}}/objects/persons"
]
}

17
wco/itm/apx.json Normal file
View File

@@ -0,0 +1,17 @@
{
"wconame": "apx",
"owner": "philc",
"price": 1,
"aliascode": [],
"commentaliascode": "if paid wco then [tribename_uniquecode,...]",
"codehash": "123",
"thumbnail": "",
"title": "Localstorage management and usefull tools",
"description": "",
"tpl": {},
"tpldata": {},
"ref": {
"Checkjson": "apxtri/objects/tplstrings/Checkjson",
"Notification": "apxtri/objects/tplstrings/Notifications", "Middlewares": "apxtri/objects/tplstrings/middlewares"
}
}

31
wco/itm/apxauth.json Normal file
View File

@@ -0,0 +1,31 @@
{
"wconame": "apxauth",
"owner": "philc",
"price": 1,
"aliascode": [],
"commentaliascode": "if paid wco then [tribename_uniquecode,...]",
"codehash": "123",
"thumbnail": "",
"title": "apXtri pagans create and authentification interface",
"description": "",
"lang": ["fr"],
"tpl": {
"apxauthmain":"apxtri/objects/wco/apxauth/main",
"apxauthscreensignup": "apxtri/objects/wco/apxauth/screensignup",
"apxauthscreensignin": "apxtri/objects/wco/apxauth/screensignin",
"apxauthscreenlogout": "apxtri/objects/wco/apxauth/screenlogout",
"apxauthscreenmytribes": "apxtri/objects/wco/apxauth/screenmytribes",
"apxauthscreeninformation": "apxtri/objects/wco/apxauth/screeninformation",
"apxauthscreenforgetkey": "apxtri/objects/wco/apxauth/screenforgetkey"
},
"tpldatamodel": { "apxauth": "apxtri/objects/wco/apxauth/exampleapxauth" },
"options": {
"profil": "{{tribeId}}/objects/options/profil"
},
"ref": {
"Odmdb": "apxtri/objects/tplstrings/Odmdb",
"Pagans": "apxtri/objects/tplstrings/Pagans",
"Persons": "apxtri/objects/tplstrings/Persons"
},
"schema": ["apxtri/objects/pagans", "{{tribe}}/objects/persons"]
}

20
wco/itm/simplemobnav.json Normal file
View File

@@ -0,0 +1,20 @@
{
"wconame": "simplemobnav",
"owner": "philc",
"price":1,
"aliascode":[],
"commentaliascode":"if paid wco then [tribename_uniquecode,...]",
"codehash": "123",
"thumbnail": "",
"title": "A simple link kist to show and hide some block for mobile screen",
"description": "",
"lang":["fr"],
"tpl": {
"simplemobnavnavbuttonh":"apxtri/objects/wco/simplemobnav/navbuttonh.mustache",
"simplemobnavnavlist": "apxtri/objects/wco/simplemobnav/navlist.mustache",
"simplemobnavnavbutton": "apxtri/objects/wco/simplemobnav/navbuttonh.mustache",
"simplemobnavmain": "apxtri/objects/wco/simplemobnav/main.mustache"},
"tpldatamodel": {
"simplemobnav":"apxtri/objects/wco/simplemobnav/examplenav"
}
}

20
wco/itm/tracker.json Normal file
View File

@@ -0,0 +1,20 @@
{
"wconame": "simplemobnavtracker",
"owner": "philc",
"price": 1,
"aliascode": [],
"commentaliascode": "if paid wco then [tribename_uniquecode,...]",
"codehash": "123",
"thumbnail": "",
"title": "A box to get cookies consent and data collection",
"description": "The tpl is independante of language this is why we store it with full extension",
"lang": [
"fr"
],
"tpl": {
"trackerconsentform": "apxtri/objects/wco/tracker/consentform.mustache"
},
"tpldatamodel": {
"trackerconsentform": "apxtri/objects/wco/tracker/exampleform"
}
}

View File

@@ -0,0 +1,99 @@
{
"contentwconame": "apxauth",
"contentid": "signature",
"logo": {
"src": "/src/static/img/logo/logobgdark.png",
"alt": "smatchit"
},
"claim": {
"textContent": "Never miss an opportunity"
},
"textlist": true,
"commentmenutype": "textlist: vertical list of menu with texte, buttonlist: horizontal btn",
"profilmenu": [
{
"mainprofil": "persons",
"link": "mytribes"
},
{
"mainprofil": "pagans",
"link": "logout"
},
{
"mainprofil": "anonymous",
"link": "signin"
}
],
"links": [
{
"link": "signup",
"label": "Pas encore d'identité apxtri ?",
"textlink": "Créer mon identité",
"tpl": "apxauthscreensignup",
"allowedprofil":["anonymous"],
"next": [
"signin",
"forgetkey",
"information"
]
},
{
"link": "signin",
"label": "S'identifier ?",
"textlink": "Accédez à vos données",
"tpl": "apxauthscreensignin",
"allowedprofil":["anonymous"],
"next": [
"signup",
"forgetkey",
"information"
]
},
{
"link": "forgetkey",
"label": "Clé oubliée ?",
"textlink": "Récupérez par email",
"tpl": "apxauthscreenforgetkey",
"allowedprofil":["anonymous"],
"next": [
"signin",
"signup",
"information"
]
},
{
"link": "information",
"label": " C'est quoi une identité apxtri ?",
"textlink": "En savoir plus",
"allowedprofil":["anonymous"],
"tpl": "apxauthscreeninformation",
"next": [
"back"
]
},
{
"link": "back",
"label": "Retour au menu ",
"allowedprofil":["anonymous"],
"tpl": "sc",
"textlink": "Retour",
"next": []
},
{
"link": "logout",
"label": " ",
"allowedprofil":["pagans"],
"tpl": "apxauthscreenlogout",
"textlink": "",
"next": []
},
{
"link": "mytribes",
"label": " ",
"tpl": "apxauthscreenmytribes",
"allowedprofil":["persons"],
"textlink": "",
"next": []
}
]
}

View File

@@ -0,0 +1,29 @@
<div class="py-1">
<img
class="mx-auto w-auto block dark:hidden"
data-wco="logo"
src="{{logobglight.src}}"
alt="{{logobglight.alt}}"
src="{{logobgdark.src}}"
alt="{{logobgdark.alt}}"
/>
<img
class="mx-auto w-auto hidden dark:block"
data-wco="logo"
src="{{logobgdark.src}}"
alt="{{logobgdark.alt}}"
/>
<h2
class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight"
data-wco="claim"
>
{{claim.textContent}}
</h2>
</div>
<div id="loading" class="flex min-h-full flex-col justify-center">
<div class="sm:mx-auto sm:w-full sm:max-w-sm py-4 text-center">
<span class="loading loading-spinner loading-lg text-secondary"></span>
</div>
</div>
<div id="{{contentid}}" wco-name="{{contentwconame}}" wco-link="{{contentscreen}}" class="mt-5 sm:mx-auto sm:w-full sm:max-w-sm"></div>
<div class="navlink"></div>

View File

@@ -0,0 +1,11 @@
<div class="flex justify-center gap-2 p-4">
{{#links}}
<button class="btn {{classnavbutton}} flex-col gap-1" onclick="apx.simplemobnav.action('{{id}}','{{link}}','{{action}}','{{wconame}}');">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-16 h-16 box-content">
<path stroke-linecap="round" stroke-linejoin="round" d="{{d}}" />
</svg>
<span class="text-sm">{{{shortlabel}}}</span>
</button>
{{/links}}
</div>

View File

@@ -0,0 +1,9 @@
{{#links}}
<p class=" mt-1 text-center text-sm text-gray-500">
{{{label}}}
<a class="font-semibold leading-6 text-secondary hover:text-primary"
onclick="apx.simplemobnav.action('{{id}}','{{link}}','{{action}}','{{wconame}}');">
{{{textlink}}}
</a>
</p>
{{/links}}

View File

View File

@@ -0,0 +1,102 @@
var apx = apx || {};
apx.simplemobnav = {};
apx.simplemobnav.loadwco = (id, ctx) => {
const tpldataname = `${apx.data.pagename}_${id}_simplemobnav`;
const simplemobnavid = document.getElementById(id)
console.log("load simplemobnav with tpldataname:", tpldataname, " id:", id, " ctx:", ctx);
let initmenu;
if (simplemobnavid.innerHTML.trim() === "") {
// Get 1st menu matching the first profil in profilmenu
apx.simplemobnav.checktpldataname(tpldataname);
for (const menulist of apx.data.tpldata[tpldataname].profilmenu) {
if (apx.data.headers.xprofils.includes(menulist.mainprofil)) {
initmenu = menulist.link
break;
}
}
ctx.link = initmenu;
apx.data.tpldata[tpldataname].contentscreen = initmenu;
simplemobnavid.innerHTML = Mustache.render(
apx.data.tpl.simplemobnavmain,
apx.data.tpldata[tpldataname]
);
} else {
//just update wco-link this will also run apx.{contentwconame}.loadwco()
simplemobnavid.querySelectorAll('[wco-name]').forEach(elt => { elt.setAttribute('wco-link', ctx.link) });
}
// shom menulist from next
const screendata = apx.data.tpldata[tpldataname].links.find(
(m) => m.link == ctx.link
);
// add in menulink the data needed to customize
const menulinks = apx.data.tpldata[tpldataname].links
.filter(m =>
screendata.next.includes(m.link) && m.allowedprofil.some(el => apx.data.headers.xprofils.includes(el)))
.map(m => {
const newm = { ...m }
if (!m.classnavbutton && apx.data.tpldata[tpldataname].classnavbutton) {
newm.classnavbutton = apx.data.tpldata[tpldataname].classnavbutton
}
if (!m.classnavlist && apx.data.tpldata[tpldataname].classnavlist) {
newm.classnavlist = apx.data.tpldata[tpldataname].classnavlist
}
return newm
})
console.log("menulminks", menulinks);
simplemobnavid.querySelector('.navlink').innerHTML = Mustache.render(
apx.data.tpl[`simplemobnav${apx.data.tpldata[tpldataname].navtpl}`],
{ id, links: menulinks }
);
document.getElementById("loading").classList.add("hidden");
console.log(`Request to show screen ${ctx.link}`);
};
apx.simplemobnav.action = (id, link, action, wconame) => {
/**
* Manage action per menu
* if navigation then it just propagate wco-link in all the wco-name component
*/
if (action == "navigation") {
document.getElementById(id).setAttribute("wco-link", link);
/*document.getElementById(id).querySelectorAll("[wco-name]").forEach(lnk => {
console.log("lnk:",lnk)
console.log(link)
lnk.setAttribute('wco-link', link)
console.log(link,lnk)
});*/
return;
}
if (!apx[wconame]) {
console.log(`%c⚠ warning:%c this requested compoment ${wconame}} does not exist`);
}
if (!apx[wconame][action]) {
console.log(`%c⚠ warning:%c this function apx.${wconame}}.${action} does not exist`);
}
apx[wconame][action]();
}
apx.simplemobnav.reload = () => {
location.reload();
}
apx.simplemobnav.checktpldataname = (tpldataname) => {
/**
* This is to help dev to build a correct json file
*/
if (!apx.data.tpldata[tpldataname]) {
console.log(`%c⚠ warning:%c ${tpldataname} does not exist in localstorage tpldata `, 'color:red;')
return false;
}
const mandatoryprop = ["contentwconame", "contentid", "profilmenu", "links"]
let missingprop = ""
mandatoryprop.forEach(p => {
if (!apx.data.tpldata[tpldataname][p]) {
missingprop += ` ${p}`
}
});
if (missingprop !== "") {
console.log(`%c⚠ warning:%c Missing property(ies) in ${tpldataname}: ${missingprop} `, 'color:red;')
return false
}
};

14
wco/testapi/nav_fr.json Normal file
View File

@@ -0,0 +1,14 @@
{ "wcocaller":"testapi",
"logo": { "src": "/src/static/img/logo/logoentete.webp", "alt": "smatchit" },
"claim": "Never miss an opportunity",
"idhome":"home",
"links": [
{
"id": "home",
"label": "Runner",
"textlink": "de test",
"tpl": "home",
"next": ["home"]
}
]
}

View File

@@ -0,0 +1,12 @@
<div class="space-y-6 text-center">
<h2>Liste de test à lancer</h2>
<p>
<a class="" onclick="alert('test1')">Test1</a>
</p>
<p>
<a class="" onclick="alert('test2')">Test2</a>
</p>
<p>
<a class="" onclick="alert('test3')">Test3</a>
</p>
</div>

462
wco/testapi/test2.js Normal file

File diff suppressed because one or more lines are too long

922
wco/testapi/testapi.js Normal file
View File

@@ -0,0 +1,922 @@
var apx = apx || {};
apx.testapi = {};
apx.testapi.getdata={}
apx.testapi.getdata.home=()=>{return {}}
apx.testapi.deleteitm = async (tribe, objectname, apxid, primaryid) => {
// example: apx.testapi.deleteitm("smatchit","sirets","siret","91365310100018")
const delitm = {
method: "delete",
url: `/api/apxtri/odmdb/itm/${tribe}/${objectname}/${apxid}/${primaryid}`,
headers: apx.data.headers,
};
const delres = await axios(delitm);
console.log(delres);
};
apx.testapi.getitm = async (tribe, objectname, primaryid) => {
console.log('get ',`/api/apxtri/odmdb/itm/${tribe}/${objectname}/${primaryid}`)
const getitm = {
method: "get",
url: `/api/apxtri/odmdb/itm/${tribe}/${objectname}/${primaryid}`,
data: {},
headers: apx.data.headers,
};
const repitm = await axios(getitm);
if (repitm.status == 200) {
if (!apx.data.itm) apx.data.itm = {};
if (!apx.data.itm[objectname]) apx.data.itm[objectname] = {};
apx.data.itm[objectname][primaryid] = repitm.data.data;
apx.save();
return repitm.data.data;
} else {
return repitm;
}
};
apx.testapi.additm = async (tribe, objectname, data) => {
const additm = {
method: "post",
url: `/api/apxtri/odmdb/itm/${tribe}/${objectname}`,
data: data,
headers: apx.data.headers,
};
return await axios(additm);
};
apx.testapi.testbase = async () => {
console.log("run")
//console.log(await apx.testapi.getitm("smatchit","seekers","philc"))
//console.log(await apx.testapi.getitm("smatchit","persons","philc"))
//console.log(await apx.testapi.getitm("smatchit","quizz","seekerknowhow_en"))
const getinfo = await axios.put(
`/api/smatchit/jobads/interviewers`,
{ jobstepids:["philc_f46b2d7e-e242-421f-802b-d8d4d02a2000_0"] },
{ headers: apx.data.headers }
);
console.log(getinfo);
//await apx.testapi.deleteitm("smatchit","sirets","siret","34921281100021")
//apx.testapi.testaddsiret();
//apx.testapi.testgetlocaldb("smatchit","smatchapp","index","anonymous");
//console.log(await apx.testapi.getitm("smatchit","quizz","seekermbti"))
/*await apx.testapi.deleteitm(
"smatchit",
"jobads",
"jobadid",
"4570dadb-949b-4f6e-9d25-f389336055c1"
);*/
//apx.testapi.testaddjobads("4570dadb-949b-4f6e-9d25-f389336055c6");
//this include jobad matching
//apx.testapi.testpublish("4570dadb-949b-4f6e-9d25-f389336055c6");
//apx.testapi.testunpublish("4570dadb-949b-4f6e-9d25-f389336055ca")
//apx.testapi.testseekerinfoforecruiter(["13cdd66c-7a22-470c-b3c9-0ba1589a1db8"],"philc")
// test jobsteps process
//refresh user data apx.testapi.getItem
//match jobads to currentuser:
/*apx.testapi.testseekerjobstepprocess(
apx.data.headers.xalias,
"4570dadb-949b-4f6e-9d25-f389336055c5"
);
*/
//connected as a seeker run to apply:
//apx.testapi.testjobstepsstateapply("4570dadb-949b-4f6e-9d25-f389336055c5");
//apx.testapi.testnotificationsendmail(["phc@ndda.fr"],"requestregistration",{email:"phc@ndda.fr",firstname:"phil", lastname:"coco"})
//apx.testapi.testseekeronboardingmatching("philc")
//apx.testapi.testbooking({ date: "2024-08-01", start: "08:30", numberofslot: 2 });
/*await apx.testapi.invitedtoapply(
"paulseek",
"4570dadb-949b-4f6e-9d25-f389336055c2",
"philc"
);*/
//apx.testapi.testrecruiteremailtoalias();
};
apx.testapi.testrecruiteremailtoalias = () => {
console.log(
"you must be a recruiter to run this request, it add alias to jobsteps where it is empty"
);
axios.get("/api/smatchit/recruiters/emailtoaliasjobstep", {
headers: apx.data.headers,
})
.then((rep) => console.log(rep))
.catch((err) => {
console.log(err);
});
};
apx.testapi.invitedtoapply = (seeker, jobadid) => {
axios
.put(
"/api/smatchit/jobads/invitedtoapply",
{ seeker: seeker, jobadid: jobadid },
{ headers: apx.data.headers }
)
.then((rep) => console.log(rep))
.catch((err) => {
console.log(err);
});
};
apx.testapi.testseekerjobstepprocess = (seeker, jobadid) => {
console.log('charge seeker info"');
//check in seeker jobadmatchscore the jobadid exist
apx.testapi.getitm("smatchit", "persons", seeker);
if (!apx.data.headers.xprofils.includes("seekers")) {
alert("Sorry must be a seeker to apply");
return false;
}
apx.testapi.getitm("smatchit", "seekers", seeker);
//await apx.testapi.testseekeronboardingmatching(seeker);
};
apx.testapi.testbook = () => {
const test = {
availableslot: {
"2024-08-10": {
"08:00": "D",
"08:30": "D",
"09:00": "D",
"09:30": "D",
"10:00": "D",
"10:30": "D",
"11:00": "D",
"11:30": "D",
"12:00": "D",
"12:30": "D",
"13:00": "D",
"13:30": "D",
"14:00": "D",
"14:30": "D",
"15:00": "D",
"15:30": "D",
"16:00": "D",
"16:30": "D",
"17:00": "D",
"17:30": "D",
"18:00": "D",
"18:30": "D",
"19:00": "D",
},
"2024-08-11": {
"08:00": "D",
"08:30": "D",
"09:00": "D",
"09:30": "D",
"10:00": "D",
"10:30": "D",
"11:00": "D",
"11:30": "D",
"12:00": "D",
"12:30": "D",
"13:00": "D",
"13:30": "D",
"14:00": "D",
"14:30": "D",
"15:00": "D",
"15:30": "D",
"16:00": "D",
"16:30": "D",
"17:00": "D",
"17:30": "D",
"18:00": "D",
"18:30": "D",
"19:00": "D",
},
},
};
console.log(test.availableslot && Object.keys(test.availableslot).length > 0);
if (
test.availableslot &&
Object.keys(test.availableslot).length > 0 &&
dayjs(Object.keys(test.availableslot).sort()[0]) > dayjs()
) {
console.log("PASSe etape act.interviewersetjobstep");
} else {
console.log("passe pas");
}
const s = { date: "2024-08-12", start: "08:00" };
console.log(
dayjs(`${s.date} ${s.start}`)
.add(30 * 2, "minutes")
.format("HH:mm")
);
};
apx.testapi.testbooking = (slottime) => {
const tirage = (probabilites) => {
//distribution {A:0.7,B:0.3} return in 70% A else B
const entries = Object.entries(probabilites);
const cumulativeProbabilities = entries.reduce((acc, [value, prob]) => {
const prevProb = acc.length > 0 ? acc[acc.length - 1][1] : 0;
return [...acc, [value, prevProb + prob]];
}, []);
const random = Math.random();
return cumulativeProbabilities.find(([value, prob]) => random < prob)[0];
};
const getlistslottime = (startslot, endslot, interval) => {
const timearray = [];
let encours = dayjs("2020-01-01 " + startslot);
const end = dayjs("2020-01-01 " + endslot);
console.log(encours);
console.log(end);
console.log(encours.isBefore(end));
while (encours.isBefore(end)) {
timearray.push(encours.format("HH:mm"));
encours = encours.add(interval, "minutes");
}
return timearray;
};
const simuleinterviewer = (interviewer) => {
// const interviewer = {startslot: "08:00",endslot: "17:00",interval: 30, availableslot: {}, };
// set futur
const arraytime = getlistslottime(
interviewer.startslot,
interviewer.endslot,
interviewer.interval
);
for (let i = 1; i < 3; i++) {
const newd = dayjs().add(i, "days").format("YYYY-MM-DD");
interviewer.availableslot[newd] = {};
arraytime.forEach(
(t) => (interviewer.availableslot[newd][t] = tirage({ F: 0.7, D: 0.3 }))
);
}
return interviewer;
};
//simule un agenda
//console.log(simuleinterviewer({startslot: "08:00",endslot: "17:00",interval: 30, availableslot: {} }))
const interviewer = {
startslot: "08:00",
endslot: "17:00",
interval: 30,
availableslot: {
"2024-07-31": {
"08:00": "F",
"08:30": "F",
"09:00": "F",
"09:30": "F",
"10:00": "D",
"10:30": "D",
"11:00": "F",
"11:30": "D",
"12:00": "F",
"12:30": "F",
"13:00": "D",
"13:30": "F",
"14:00": "D",
"14:30": "F",
"15:00": "F",
"15:30": "D",
"16:00": "D",
"16:30": "D",
},
"2024-08-01": {
"08:00": "F",
"08:30": "F",
"09:00": "F",
"09:30": "D",
"10:00": "F",
"10:30": "F",
"11:00": "F",
"11:30": "D",
"12:00": "D",
"12:30": "F",
"13:00": "F",
"13:30": "D",
"14:00": "F",
"14:30": "D",
"15:00": "F",
"15:30": "F",
"16:00": "F",
"16:30": "F",
},
},
};
//{ date: "2024-08-01", start: "08:30", numberofslot: 2 }
let matchslottime = true;
matchslottime = interviewer.availableslot[slottime.date] ? true : false;
matchslottime = interviewer.availableslot[slottime.date][slottime.start]
? true
: false;
matchslottime =
interviewer.availableslot[slottime.date][slottime.start] == "F";
let slottobook = [slottime.start];
console.log("1er slot:", matchslottime);
if (matchslottime) {
const slotarray = Object.keys(
interviewer.availableslot[slottime.date]
).sort();
console.log(slotarray);
for (
i = slotarray.indexOf(slottime.start) + 1;
i < slottime.numberofslot + 1;
i++
) {
matchslottime = interviewer.availableslot[slottime.date][i] != "F";
slottobook.push(slotarray[i]);
}
}
console.log(slottobook);
if (matchslottime) {
slottobook.forEach((t) => {
interviewer.availableslot[slottime.date][t] = "J";
});
console.log("booking", interviewer.availableslot[slottime.date]);
} else {
console.log("no booking:");
}
};
apx.testapi.testseekeronboardingmatching = async (seeker) => {
await axios
.get(`/api/smatchit/seekers/onboarding/${seeker}`, {
headers: apx.data.headers,
})
.then((rep) => {
//alert("success, check console");
console.log(rep);
})
.catch((err) => {
//alert("err, check console");
console.log(err);
});
};
apx.testapi.testnotificationsendmail = async (emails, template, data) => {
await axios
.post(
`/api/apxtri/notifications/sendmail/smatchit/${template}`,
{ emails, data },
{ headers: apx.data.headers }
)
.then((rep) => {
alert("success, check console");
console.log(rep);
})
.catch((err) => {
alert("err, check console");
console.log(err);
});
};
apx.testapi.testseekerinfoforecruiter = async (jobadids, aliasrecruiter) => {
// must be connectered as aliasrecruitrer or to have adminrecruiter profil for the same siret than jobads
await axios
.put(
`/api/smatchit/seekers/infoseeker/${aliasrecruiter}`,
{ jobadids: jobadids },
{ headers: apx.data.headers }
)
.then((rep) => {
alert("success, check console");
console.log(rep);
})
.catch((err) => {
alert("err, check console");
console.log(err);
});
};
apx.testapi.testjobstepsstateapply = async (jobadid) => {
//create a jobstep
console.log("getitm");
const jobad = await apx.testapi.getitm("smatchit", "jobads", jobadid);
if (jobad.status) {
console.log("issue to get jobad", getjobad);
}
console.log(jobad);
const jobstep = {
jobadid: jobadid,
recruiter: jobad.recruiter,
interviewer: jobad.jobsteps[0].interviewer,
jobstepid: `${apx.data.headers.xalias}_${jobadid}_0`,
seeker: apx.data.headers.xalias,
state: "apply",
};
console.log(jobstep);
//const apply = await apx.testapi.additm("smatchit", "jobsteps", jobstep);
//console.log(apply);
console.log(`/api/smatchit/jobads/jobstepstodo/${jobstep.jobstepid}`);
const jobstepstodo = await axios.put(
`/api/smatchit/jobads/jobstepstodo/${jobstep.jobstepid}`,
{ state: "apply" },
{ headers: apx.data.headers }
);
console.log(jobstepstodo);
};
apx.testapi.testunpublish = async (jobadid) => {
await axios
.put(
`/api/smatchit/jobads/archive/${jobadid}`,
{},
{ headers: apx.data.headers }
)
.then((rep) => {
alert("success, check console");
console.log(rep);
})
.catch((err) => {
alert("err, check console");
console.log(err);
});
};
apx.testapi.testpublish = async (jobadid) => {
await axios
.put(
`/api/smatchit/jobads/publish/${jobadid}`,
{},
{ headers: apx.data.headers }
)
.then((rep) => {
//alert("success, check console");
console.log(rep);
})
.catch((err) => {
//alert("err, check console");
console.log(err);
});
};
apx.testapi.testaddjobads = (jobadid) => {
/*const jobad = {
jobadid: jobadid,
state: "inprocess",
siret: "34921281100021",
category: "gaming",
jobtitle: "cuisinier",
jobdisplayname: "Cuisinier poissonier",
contactrecruiter: "philc",
state: "draft",
recruiter: "philc",
jobadtitle: "Cuisinier poissonier",
candidateexperience: "4to12",
specificskills: {
reserveeventticket: 2,
teamcoordination: 3,
aftersellservice: 2,
},
languageskills: {
french: 4,
english: 2,
},
knowhows: ["service", "reactif", "inspire"],
dealbreaker: ["ponctualitty"],
jobtype: ["cdi", "cdd", "interim", "freelance", "stage", "alternance"],
fulltime: "fullandpartial",
remote: 100,
workingdayshours: [
{
day: "Tuesday",
hours: [
{ start: "09:00", end: "00:00" },
{ start: "11:00", end: "00:00" },
],
},
],
salary: 100000,
salarydevise: "€",
salaryunit: "peryear",
jobsteps: [
{
jobsteptype: "firstcontact",
interviewer: "philc",
},
{
jobsteptype: "testaptitude",
interviewer: "interviewerpeter",
},
{
jobsteptype: "interview",
interviewer: "philc",
},
],
description:
"Hello, About Job \\nLorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
};*/
const jobad = {
jobadid: jobadid,
state: "publish",
siret: "42818175400014",
category: "Automobile",
jobtitle: "vendorretail",
jobdisplayname: "Épicière",
jobadtitle: "Épicière Test",
candidateexperience: "12to15",
specificskills: {
productvalidity: 3,
trackproduct: 1,
informclient: 1,
chooseintervention: 0,
reserveeventticket: 2,
teamcoordination: 3,
aftersellservice: 3,
adaptsellstrategy: 3,
createsellarguments: 3,
technicalexpertise: 3,
instructemployee: 3,
deliverorder: 3,
},
knowhows: ["teamwork", "accurate", "service", "listening", "fits"],
dealbreaker: ["ponctualitty"],
languageskills: {
french: 4,
english: 1,
},
jobtype: ["cdi", "freelance", "stage"],
fulltime: "full",
remote: 50,
workingdayshours: [
{
day: "Tuesday",
hours: [
{
start: "10",
end: "00",
},
{
start: "19",
end: "00",
},
],
},
],
salary: 100000,
salarydevise: "€",
salaryunit: "permonth",
jobsteps: [
{
jobsteptype: "firstcontact",
interviewer: "philc",
email: "smatchitdev@mailsac.com",
},
{
jobsteptype: "testaptitude",
interviewer: "philc",
email: "smatchitdev@mailsac.com",
},
{
jobsteptype: "interview",
interviewer: "philc",
email: "smatchitdev@mailsac.com",
},
{
jobsteptype: "call",
interviewer: "philc",
email: "smatchitdev@mailsac.com",
},
],
description:
'Contrary to popular belief, Lorem Ipsum is not simply random text. It is rooted in a piece of classical Latin literature from 45 BC, over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, trendy during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.',
recruiter: "philc",
urgenthiring: false,
jobadmbti: "",
jobadlocation: [],
critrulesalary: "",
owner: "philc",
dt_create: "2024-08-02T06:22:25.720Z",
};
console.log(apx.testapi.additm("smatchit", "jobads", jobad));
};
apx.testapi.testaddsiret = () => {
const siret = {
siret: "34921281100021",
businessname: "SAAS",
code_naf: "92.3H",
tradename: "SAAS",
category: "gaming",
billinglocation: {
type: "housenumber",
housenumber: "25",
street: "Rue de Ponthieu",
name: "25 Rue de Ponthieu",
label: "25 Rue de Ponthieu 75008 Paris",
postcode: "75008",
citycode: "75108",
city: "Paris",
position: {
properties: { longitude: 2.309055, latitude: 48.870756 },
},
context: "75, Paris, Île-de-France",
},
businesslocation: {
type: "housenumber",
housenumber: "25",
street: "Rue de Ponthieu",
name: "25 Rue de Ponthieu",
label: "25 Rue de Ponthieu 75008 Paris",
postcode: "75008",
citycode: "75108",
city: "Paris",
position: {
properties: { longitude: 2.309055, latitude: 48.870756 },
},
context: "75, Paris, Île-de-France",
},
website: "https://www.google.com",
socialnetworks: ["abcd"],
title: "Hello Compnay",
description: "lets good 👍 ",
agreetorespectnorms: true,
};
const addsiret = {
method: "post",
url: `/api/apxtri/odmdb/itm/smatchit/sirets`,
data: siret,
headers: apx.data.headers,
};
axios(addsiret)
.then((rep) => {
alert("success, check console");
console.log(rep);
})
.catch((err) => {
alert("err, check console");
console.log(err);
});
};
apx.testapi.testdeviceid = () => {
function test() {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
alert("enumerateDevices() not supported.");
return;
}
// List cameras and microphones.
navigator.mediaDevices
.enumerateDevices()
.then(function (devices) {
devices.forEach(function (device) {
alert(device.kind + ": " + device.label + " id = " + device.deviceId);
});
})
.catch(function (err) {
alert(err.name + ": " + err.message);
});
}
test();
};
apx.testapi.testdeletealias = async () => {
const aliastodel = ["totopoursup"];
aliastodel.forEach((a) => {
const axiosdel = {
method: "delete",
url: `/api/apxtri/pagans/person/smatchit/${a}`,
data: newseeker,
headers: apx.data.headers,
};
axios(axiosdel)
.then((rep) => {
alert("success, check console");
console.log(rep);
})
.catch((err) => {
alert("err, check console");
console.log(err);
});
});
};
apx.testapi.testaddseeker = async () => {
const newseeker = {
alias: "philc",
email: "bhavesh@mailsac.com",
locationenabled: true,
notificationenabled: true,
seekstatus: "notlooking",
seekworkingyear: "4to8",
seekjobtitleexperience: ["chefpartie"],
seeklocation: [
{
type: "street",
housenumber: "",
street: "Igny",
name: "Igny",
label: "Igny 18200 La Perche",
postcode: "18200",
citycode: "18178",
city: "La Perche",
zoning: 3,
position: { properties: { longitude: 2.585915, latitude: 46.638536 } },
context: "18, Cher, Centre-Val de Loire",
},
],
salaryexpectation: "2331",
salaryunit: "permonth",
salarydevise: "€",
mbti: { E: 1, N: 3, T: 0, nextq: 4, value: "INFP" },
};
const axiosaddseeker = {
method: "post",
url: `/api/apxtri/odmdb/itm/smatchit/seekers`,
data: newseeker,
headers: apx.data.headers,
};
const addseek = await axios(axiosaddseeker);
console.log("profil update", addseek);
};
apx.testapi.testremoveprofilrecruiter = async () => {
const user = "philc";
const axiosgetperson = {
method: "delete",
url: `/api/smatchit/persons/recruiters/${user}`,
headers: apx.data.headers,
};
const axiosaddrecruiters = {
method: "put",
url: `/api/apxtri/pagans/person/smatchit`,
data: { addprofils: "recruiters", alias: user },
headers: apx.data.headers,
};
const addrec = await axios(axiosaddrecruiters);
console.log("profil update", addrec);
const axiosperson = await axios(axiosgetperson);
if (axiosperson.status != 200) {
console.log("erreur getting person");
return false;
} else {
console.log("lecture person", axiosperson);
}
};
apx.testapi.testaddprofilrecruiter = async () => {
const user = "philc";
const axiosgetperson = {
method: "get",
url: `/api/apxtri/odmdb/itm/smatchit/persons/${user}`,
headers: apx.data.headers,
};
const axiosaddrecruiters = {
method: "put",
url: `/api/apxtri/pagans/person/smatchit`,
data: { addprofils: "recruiters", alias: user },
headers: apx.data.headers,
};
const addrec = await axios(axiosaddrecruiters);
console.log("profil update", addrec);
const axiosperson = await axios(axiosgetperson);
if (axiosperson.status != 200) {
console.log("erreur getting person");
return false;
} else {
console.log("lecture person", axiosperson);
}
};
apx.testapi.testseekerprofil = async () => {
alert("get person and modify ");
//get a person
const user = "philc";
const axiosgetperson = {
method: "get",
url: `/api/apxtri/odmdb/itm/smatchit/persons/${user}`,
headers: apx.data.headers,
};
const axiosputperson = {
method: "put",
url: `/api/apxtri/odmdb/itm/smatchit/persons/`,
data: {},
headers: apx.data.headers,
};
let userperson;
const axiosperson = await axios(axiosgetperson);
if (axiosperson.status != 200) {
console.log("erreur getting person");
return false;
}
userperson = axiosperson.data.data;
console.log(userperson);
let updateperson = {
alias: user,
firebaseid: userperson.firebaseid + "test",
profils: ["seekers"],
};
axiosputperson.data = updateperson;
console.log(axiosputperson);
const axiosupdateperson = await axios(axiosputperson);
console.log("modification done", axiosupdateperson);
//alert("change again to remove test in firebase");
updateperson = {
alias: user,
firebaseid: userperson.firebaseid.replace(/test/g, ""),
};
axiosputperson.data = updateperson;
console.log(axiosputperson);
const axiosupdatepersonback = await axios(axiosputperson);
console.log(axiosupdatepersonback);
};
apx.testapi.testgetlocaldb = (tribe, appname, pagename, anonymous) => {
const testaxios = {
method: "get",
url: `/api/apxtri/wwws/updatelocaldb${anonymous}/${tribe}/${appname}/${pagename}/0`,
headers: apx.data.headers,
};
axios(testaxios)
.then((rep) => {
alert("success, check console");
console.log(rep);
})
.catch((err) => {
alert("err, check console");
console.log(err);
});
};
apx.testapi.testgetperson = () => {
const testaxios = {
method: "get",
url: `/api/apxtri/odmdb/itm/${apx.data.headers.xtribe}/persons/philc`,
headers: apx.data.headers,
};
axios(testaxios)
.then((rep) => {
alert("success, check console");
console.log(rep);
})
.catch((err) => {
alert("err, check console");
console.log(err);
});
};
apx.testapi.testupdateseeker = () => {
apx.data.itm.seekers.jobadinvitedtoapply = [
"f46b2d7e-e242-421f-802b-d8d4d02a2004",
];
const testaxios = {
method: "put",
url: `/api/apxtri/odmdb/itm/${apx.data.headers.xtribe}/seekers`,
data: apx.data.itm.seekers,
headers: apx.data.headers,
};
axios(testaxios)
.then((rep) => {
alert("success, check console");
console.log(rep);
})
.catch((err) => {
alert("err, check console");
console.log(err);
});
};
apx.testapi.testbackend = () => {
const objsearch = {
apxid: [
"f46b2d7e-e242-421f-802b-d8d4d02a2000",
"f46b2d7e-e242-421f-802b-d8d4d02a2001",
"f46b2d7e-e242-421f-802b-d8d4d02a2002",
"f46b2d7e-e242-421f-802b-d8d4d02a2003",
],
fields: "all",
};
const objsearchaxios = {
method: "post",
url: `/api/apxtri/odmdb/searchitms/${apx.data.headers.xtribe}/jobads`,
data: objsearch,
headers: apx.data.headers,
};
axios(objsearchaxios)
.then((rep) => {
alert("success, check console");
console.log(rep);
})
.catch((err) => {
alert("err, check console");
console.log(err);
});
};
/*
apx.testapi.apxlocal = {};
apx.testapi.apxlocal.setup = () => {
console.log("run apxlocal.setup");
apx.data.tpldata.apxlocal.categories.forEach((cat, i) => {
//Object.keys(apx.data[cat.categorie]).forEach((o)=>{apx.data.tpldata.apxlocal.categories[i].list=[]})
Object.keys(apx.data[cat.categorie]).forEach((o) => {
if (!apx.data.tpldata.apxlocal.categories[i].list.includes(o)) {
apx.data.tpldata.apxlocal.categories[i].list.push(o);
}
});
});
document
.getElementsByName("apxlocal")
.forEach(
(e) =>
(e.innerHTML = Mustache.render(
apx.data.tpl.apxlocal,
apx.data.tpldata.apxlocal
))
);
dscreen.refresh();
};
apx.readyafterupdate(apx.testapi.apxlocal.setup);
*/

View File

@@ -0,0 +1,33 @@
<style>
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-100px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 2s ease-out;
}
</style>
<div class="tracker bg-white rounded-lg shadow-lg p-6 w-full max-w-md md:max-w-lg lg:max-w-xl xl:max-w-2xl animate-fadeIn">
<!-- Texte d'information sur les cookies -->
<div class="mb-4">
<p class="text-sm text-gray-700">
{{introtext}}
<a href="{{CGU}}" target="_blank" class="text-blue-500 hover:underline">{{{CGUlabel}}}</a>
</p>
</div>
<!-- Boutons d'action -->
<div class="flex flex-col space-y-2">
{{#btn}}
<button class="btn btn-primary w-full" onclick="apx.tracker.setconsent('{{tagid}}','{{action}}')">
{{{text}}}
</button>
{{/btn}}
</div>
</div>

View File

@@ -0,0 +1,19 @@
{
"CGU": "https://smatchit.io/Smatchit_RGPD_app_web_fr.html",
"CGUlabel": "Terms of Service",
"introtext": "In accordance with the GDPR, we need your permission to store your personal data on this site. If you 'refuse all recording', you will not have access to the content with identification. If you wish to support us, your data may be shared with third parties. Please consult our terms and conditions for more information:",
"btn": [
{
"action": "acceptfullcookies",
"text": "I accept to support this site"
},
{
"action": "acceptstatisticcookies",
"text": "I accept only to facilitate navigation"
},
{
"action": "refusecookies",
"text": "I refuse all recording"
}
]
}

View File

@@ -0,0 +1,19 @@
{
"CGU": "https://smatchit.io/Smatchit_RGPD_app_web_fr.html",
"CGUlabel": "Conditions générales d'utilisation",
"introtext": "Conformément au RGPD, nous devons obtenir votre autorisation pour stocker vos données personnelles sur ce site. Si vous 'refusez tout enregistrement', vous n'aurez pas accès au contenu avec identification. Si vous souhaitez nous soutenir, vos données pourront être communiquées à des tiers. Consultez nos conditions pour en savoir plus :",
"btn": [
{
"action": "acceptfullcookies",
"text": "J'accepte pour soutenir ce site"
},
{
"action": "acceptstatisticcookies",
"text": "J'accepte uniquement pour faciliter la navigation"
},
{
"action": "refusecookies",
"text": "Je refuse tout enregistrement"
}
]
}

140
wco/tracker/tracker.js Normal file
View File

@@ -0,0 +1,140 @@
var apx = apx || {};
apx.tracker = {};
apx.tracker.getinfodevice = async () => {
const device = {};
//console.log(navigator);
device.useragent = navigator.userAgent || navigator.vendor || window.opera;
device.typedevice = /iPad/i.test(device.useragent) ? "ipad" : "";
device.typedevice = /iPhone/i.test(device.useragent) ? "iphone" : "";
device.typedevice = /iPod/i.test(device.useragent) ? "ipod" : "";
device.typedevice = /Android/i.test(device.useragent) ? "android" : "";
device.typedevice =
device.typedevice == "" &&
/Windows NT|Macintosh|Linux/i.test(device.useragent)
? "PC"
: "";
console.log(
"test linux",
/Windows NT|Macintosh|Linux/i.test(device.useragent)
);
device.type =
device.type === "" && /Mobi/i.test(device.userAgent) ? "mobile" : "";
device.os = /Windows NT/i.test(device.useragent) ? "windows" : "";
device.os = /Macintosh/i.test(device.useragent) ? "mac" : "";
device.os = /Linux/i.test(device.useragent) ? "linux" : "";
const ipinfo = await axios.get("https://ipinfo.io/json");
console.log(ipinfo);
if (
ipinfo &&
ipinfo.data &&
ipinfo.data !== null &&
typeof ipinfo.data === "object" &&
Object.keys(ipinfo.data).length > 0
) {
if (ipinfo.data.ip) device.ip = ipinfo.data.ip;
if (ipinfo.data.city) device.city = ipinfo.data.city;
if (ipinfo.data.country) device.country = ipinfo.data.country;
}
device.screenWidth = window.screen.width;
device.screenHeight = window.screen.height;
const connection =
navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection;
if (connection) {
device.connection = `${connection.effectiveType}_${connection.downlink} Mbps`;
}
device.lang = navigator.language;
device.plugins = Array.from(navigator.plugins).map((plugin) => plugin.name);
//console.log(device)
return device;
};
apx.tracker.hit = (data) => {
if (!data.consentcookie && localStorage.getItem("consentcookie"))
data.consentcookie = localStorage.getItem("consentcookie");
if (!data.lasttm && localStorage.getItem("lasttm"))
data.lasttm = localStorage.getItem("lasttm");
if (!data.xuuid && localStorage.getItem("xuuid"))
data.xuuid = localStorage.getItem("xuuid");
if (!data.xapp && localStorage.getItem("xapp"))
data.xapp = localStorage.getItem("xapp");
if (apx.data.headers.xalias != "anonymous")
data.alias = apx.data.headers.xalias;
let missing = "";
[("xapp", "srckey", "xuuid", "lasttm", "consentcookie")].forEach((k) => {
missing += !data[k] ? `${k},` : "";
});
if (missing !== "") {
console.log(`Check your trktag ${missing} are missing`);
return;
}
let urltrack = "/trk/cdn/trkret/empty.json?";
Object.keys(data).forEach((d) => {
urltrack += `d=${data[d]}&`;
});
urltrack = urltrack.slice(0, -1);
//console.log(urltrack);
axios.get(urltrack);
};
apx.tracker.load = () => {
if (!localStorage.getItem("consentcookie")) {
document
.querySelectorAll('div:not([wco-name="tracker"])')
.forEach((el) => {
el.classList.add("opacity-40");
el.style.pointerEvents = "auto";
});
document.querySelectorAll("[wco-name='tracker']").forEach((el) => {
const tpldata = `${apx.data.pagename}_${el.id}_trackerconsentform`;
if (!apx.data.tpldata[tpldata] || !apx.data.tpldata[tpldata].introtext) {
console.log(` ${tpldata} does not exist check your file`);
} else {
el.innerHTML = Mustache.render(
apx.data.tpl.trackerconsentform,
apx.data.tpldata[tpldata]
);
}
});
} else {
// test if last loading was <10minutes then send a new tag
const tm = dayjs().valueOf();
const lasttmtxt = localStorage.getItem("lasttm");
const lasttm = lasttmtxt ? Number(lasttmtxt) : null;
//console.log(lasttm, tm, tm - lasttm > 10 * 60 * 1000);
if (lasttm && tm - lasttm > 10 * 60 * 1000) {
localStorage.setItem("lasttm", tm);
apx.tracker.hit({ srckey: "visitwwws" });
}
}
};
apx.tracker.setconsent = async (tagid, choice) => {
//localStorage.setItem("consentcookie", choice);
if (!localStorage.getItem("xuuid")) {
const uuid = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
);
localStorage.setItem("xuuid", uuid);
apx.data.headers.xuuid = uuid;
const tm = dayjs().valueOf();
localStorage.setItem("lasttm", tm);
apx.save();
}
if (["acceptstatisticcookies", "acceptfullcookies"].includes(choice)) {
const infodevice = await apx.tracker.getinfodevice();
//console.log(infodevice);
axios.post(`/api/apxtri/trackings/newdevice`, infodevice, {
headers: apx.data.headers,
});
}
apx.tracker.hit({ srckey: "firstvisitwwws" });
document.querySelectorAll('div:not([wco-name="tracker"])').forEach((el) => {
el.classList.remove("opacity-40");
el.style.pointerEvents = "auto";
});
document.getElementById(tagid).classList.add("hidden");
};
apx.readyafterupdate(apx.tracker.load);

View File

@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="fr" data-theme="apxtri" class="h-full bg-neutral">
<head>
<meta charset="utf-8" />
<title>Admin Template</title>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<meta
content="saas, apxtri"
name="keywords"
/>
<meta
content="A saas solution that you can host at home to master your data"
name="description"
/>
<link data-wco="favicon" href="static/img/icons/iconbgdark.png" rel="icon" />
<link href="static/css/twstyle.css" rel="stylesheet" />
<script>
/**
* Read apx.js to know more
*/
const apxtri = {
headers: {
xtrkversion: 1,
xtribe: "apxtri",
xapp: "admin",
xlang: "fr",
xalias: "anonymous",
xhash: "anonymous",
xprofils:["anonymous"],
xdays: 0,
},
pagename: "admindata",
pageauth: "apxid",
allowedprofils:["anonymous"],
searchfunction:"adminsearch", // must exist a function call apx.adminsearch(search) that return a relevant result check example
version:0
};
</script>
<script src="/apxtri/node_modules/axios/dist/axios.min.js"></script>
<script src="/apxtri/node_modules/dayjs/dayjs.min.js"></script>
<script src="/apxtri/node_modules/openpgp/dist/openpgp.min.js"></script>
<script src="/apxtri/node_modules/mustache/mustache.min.js"></script>
<script src="/apxtri/Checkjson.js"></script>
<script src="/api/apxtri/wwws/getwco/apx.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=admindata&code=enjoy"></script>
<script src="/api/apxtri/wwws/getwco/adminskull.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=admindata&code=enjoy"></script>
<script src="static/js/adminsearch.js"></script>
</head>
<body class="bg-gray-100">
<div class="flex min-h-screen">
<!-- Vertical Navigation Menu -->
<!--button onclick="apx.adminskull.togglesidebarmobile(this)" class="sm:hidden fixed top-4 left-4 z-50 p-2 bg-base-200 rounded-lg shadow">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
</svg>
</button-->
<nav id="sidebar" class="bg-neutral-800 text-white fixed sm:relative sm:w-64 w-0 transform transition-all duration-300 ease-in-out -translate-x-full sm:translate-x-0 p-4 shadow-lg z-40 overflow-hidden hidden flex flex-col h-screen">
<!--nav id="sidebar" class="hidden sm:block bg-neutral-800 text-white w-64 h-screen flex flex-col shadow-lg transition-width"-->
<!-- User Profile Section -->
<div class="p-4">
<div class="flex items-center justify-between"> <!-- Ajout de justify-between pour aligner la croix à droite -->
<!-- Image et texte -->
<div class="flex items-center">
<img data-wco="favicon" src="static/img/icons/iconbglight.png" class="rounded-full h-9 w-9 mr-3" alt="apXtri">
<div>
<h2 data-wco="companyname" class="text-base font-semibold">apXtri</h2>
<p data-wco="claim" class="text-sm text-gray-400">L'unique et sa propriété</p>
</div>
</div>
<!-- Icône de croix (fermeture) -->
<button onclick="apx.adminskull.togglesidebarmobile(this)" class="text-gray-400 hover:text-white sm:hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- Menu Links Section -->
</nav>
<!-- Main Content Area -->
<div class="flex-1 bg-gray-100">
<!-- Horizontal Navigation Bar -->
<header class="bg-white shadow-md p-4">
<div id="headnav" class="flex items-center justify-between">
</div>
</header>
<div id="maincontent" class=""> Contenu of id=maincontent</div>
<div id="searchcontent" class=" hidden flex flex-col border border-gray-200 rounded-lg p-4">
<!-- Ligne avec la flèche de retour -->
<div class="flex justify-between items-center mb-4">
<h2 id="searchtitle" class="text-lg font-semibold">Résultat sur: </h2>
<!-- Flèche de retour (icône DaisyUI) -->
<button class="btn btn-circle btn-sm btn-ghost" onclick="document.getElementById('searchcontent').classList.add('hidden');document.getElementById('maincontent').classList.remove('hidden');">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</button>
</div>
<!-- Div de contenu -->
<div id="searchresults" class="bg-base-200 text-base-content p-4 rounded-lg">
<p>Show search result here...</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="fr" data-theme="apxtridark" class="h-full bg-base-200 text-neutral-content ">
<head>
<meta charset="utf-8" />
<title>Authentification</title>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<meta
content="L'unique et sa propriété, authentification, apxtri, cle public, cle privée"
name="keywords"
/>
<meta
content="Porte d'entrée dans l'univers libre d'apXtri, là où vous pouvez être l'Unique et sa propriété."
name="description"
/>
<link data-wco="favicon" href="static/img/icons/iconbgdark.png" rel="icon" />
<!--script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script-->
<link href="static/css/output.css" rel="stylesheet" />
<script>
/**
* Read apx.js to know more
*/
const apxtri = {
headers: {
xtrkversion: 1,
xtribe: "apxtri",
xapp: "admin",
xlang: "fr",
xalias: "anonymous",
xhash: "anonymous",
xprofils:["anonymous"],
xdays: 0,
xuuid:0
},
pagename: "apxid",
pageauth: "apxid",
wcoobserver:true,
allowedprofils:["anonymous"],
version:0
};
</script>
<script src="/apxtri/node_modules/axios/dist/axios.min.js"></script>
<script src="/apxtri/node_modules/dayjs/dayjs.min.js"></script>
<script src="/apxtri/node_modules/openpgp/dist/openpgp.min.js"></script>
<script src="/apxtri/node_modules/mustache/mustache.min.js"></script>
<script src="/apxtri/Checkjson.js"></script>
<script src="/api/apxtri/wwws/getwco/apx.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy"></script>
<script src="/api/apxtri/wwws/getwco/simplemobnav.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy&tagid=authentification"></script>
<script src="/api/apxtri/wwws/getwco/apxauth.js?wcotribe=apxtri&tribe=apxtri&xapp=admin&pagename=apxid&code=enjoy&tagid=signature"></script>
</head>
<body class="h-full">
<div class="flex items-center justify-center min-h-screen px-4">
<div
id="authentification"
wco-name="simplemobnav"
class="bg-base-100 min-h-screen w-full p-4 text-center">
</div>
<!--div wco-name="chatroom" class="hidden min-h-full flex-col justify-center px-6 py-12 lg:px-8">
</div-->
</div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
<div class="mb-4 p-4 bg-green-50 rounded-lg shadow">
<p class="text-green-600">Currently active</p>
<p class="text-gray-800">Job Ad Title {{jobAdNumber}}</p>
<p class="text-gray-500">Expiry Date</p>
<p class="text-gray-800">DD month YYYY (X days remaining)</p>
</div>

View File

@@ -0,0 +1,26 @@
.attention-message {
text-align: center;
}
.attention-message-heading,
.attention-message-subheading {
color: #F04438;
font-family: 'Inter', sans-serif;
font-size: 18px;
font-style: normal;
font-weight: 700;
line-height: 28px;
}
.attention-message-subheading {
margin-bottom: 16px;
}
.attention-message-body {
color: #18191E;
font-family: 'Inter', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
}

View File

@@ -0,0 +1,5 @@
<div class="attention-message p-4">
<p class="attention-message-heading">Attention:</p>
<p class="attention-message-subheading">You currently have X job ads active.</p>
<p class="attention-message-body">When your plan expires, all active job ads will be removed and candidates notified the job has been canceled lorem ipsum.</p>
</div>

View File

@@ -0,0 +1,28 @@
.button-container {
display: flex;
width: 325px;
height: 48px;
justify-content: center;
align-items: center;
background-color: var(--app-button-background, #000);
border-radius: 4px;
}
.button-label {
color: var(--_app-global-grayscale-50, #F9FAFB);
text-align: center;
font-family: 'Inter', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 900;
line-height: 20px;
text-transform: uppercase;
margin-right: 8px;
}
.button-icon {
width: var(--Spacing-Link-Below-Textfield, 18px);
height: var(--Spacing-Link-Below-Textfield, 18px);
flex-shrink: 0;
}

View File

@@ -0,0 +1,3 @@
<button class="btn w-full flex justify-between items-center mb-2" id="button-element">
<span id="button-text">BUTTON TEXT</span>
</button>

View File

@@ -0,0 +1,6 @@
function initializeButton(buttonText, buttonAction) {
const buttonElement = document.getElementById('button-element');
buttonElement.innerText = buttonText;
buttonElement.addEventListener('click', buttonAction);
}

View File

@@ -0,0 +1,16 @@
<div class="job-ad-detail p-4 bg-white rounded-lg shadow mb-4">
<p class="text-green-500">Currently active</p>
<p class="text-gray-800">Job Ad Title #1</p>
<p class="text-gray-600">Expiry Date</p>
<p class="text-gray-800">DD month YYYY (X days remaining)</p>
</div>
<div class="job-ad-detail p-4 bg-white rounded-lg shadow mb-4">
<p class="text-green-500">Currently active</p>
<p class="text-gray-800">Job Ad Title #2</p>
<p class="text-gray-600">Expiry Date</p>
<p class="text-gray-800">DD month YYYY (X days remaining)</p>
</div>
<div class="job-ad-detail p-4 bg-white rounded-lg shadow mb-4">
<p class="text-gray-500">Ready to be used / Not active</p>
<p class="text-gray-800">Job Ad Title #3</p>
</div>

View File

@@ -0,0 +1,19 @@
function showExtraJobAdsTab() {
document.getElementById('extra-job-ads-content').classList.remove('hidden');
document.getElementById('extra-job-ads-tab').classList.add('custom-tab-active');
document.getElementById('extra-job-ads-tab').classList.remove('custom-tab-inactive');
loadComponent('buttons-container', 'components/Button.html', () => {
document.getElementById('button-text').innerText = 'GET EXTRA JOB ADS';
const buttonElement = document.getElementById('button-element');
buttonElement.classList.add('bg-black', 'text-white');
buttonElement.innerHTML += `
<svg class="h-6 w-6 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
`;
});
}

View File

@@ -0,0 +1,7 @@
<div class="flex justify-start mb-4">
<button class="btn btn-circle btn-outline" id="close-button">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>

View File

@@ -0,0 +1,5 @@
<div class="mb-4 p-4 bg-gray-50 rounded-lg shadow">
<p class="text-gray-600">Ready to be used / Not active</p>
<p class="text-gray-800">Job Ad Title {{jobAdNumber}}</p>
</div>

View File

@@ -0,0 +1,7 @@
<div class="mb-4 p-4 bg-gray-50 rounded-lg shadow">
<p class="text-gray-500" id="job-ad-status"></p>
<p class="text-gray-800" id="job-ad-title"></p>
<p class="text-gray-600" id="job-ad-expiry-label"></p>
<p class="text-gray-800" id="job-ad-expiry"></p>
</div>

View File

@@ -0,0 +1,53 @@
.job-ad-detail {
display: flex;
flex-direction: column;
padding: 16px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
}
.active-job-ad {
background-color: #f9fafb;
}
.inactive-job-ad {
background-color: #f3f4f6;
}
.job-ad-title {
font-family: 'Inter', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px;
color: var(--app-text-stroke-text-stroke-day, #18191E);
margin-bottom: 16px;
}
.status {
font-family: 'Inter', sans-serif;
font-weight: 600;
font-size: 14px;
line-height: 20px;
margin-bottom: 8px;
}
.job-title {
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 20px;
margin-top: 8px;
margin-bottom: 16px;
}
.expiry-label,
.expiry-date {
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 20px;
margin-top: 8px;
}

View File

@@ -0,0 +1,72 @@
function generateJobAds(activeCount, inactiveCount) {
const container = document.getElementById('extra-job-ads-content');
container.innerHTML = ''; // Clear existing content
// Update the tab text with the total number of job ads
const totalJobAds = activeCount + inactiveCount;
const extraJobAdsTab = document.getElementById('extra-job-ads-tab');
extraJobAdsTab.textContent = `Extra job ads (${totalJobAds})`;
// Generate active job ads
for (let i = 1; i <= activeCount; i++) {
const activeAdContainer = document.createElement('div');
activeAdContainer.className = 'job-ad-container mb-4';
const activeAdTitle = document.createElement('p');
activeAdTitle.className = 'job-ad-title text-gray-800';
activeAdTitle.textContent = `Extra job ad #${i} (currently in use)`;
const activeAd = document.createElement('div');
activeAd.className = 'job-ad-detail active-job-ad p-4 bg-white rounded-lg shadow';
activeAd.innerHTML = `
<p class="text-green-500 status">Currently active</p>
<p class="text-gray-800 job-title">&lt;Job ad title&gt;</p>
<p class="text-gray-600 expiry-label">Expiry Date</p>
<p class="text-gray-800 expiry-date">DD month YYYY (X days remaining)</p>
`;
activeAdContainer.appendChild(activeAdTitle);
activeAdContainer.appendChild(activeAd);
container.appendChild(activeAdContainer);
}
// Generate inactive job ads
for (let i = 1; i <= inactiveCount; i++) {
const inactiveAdContainer = document.createElement('div');
inactiveAdContainer.className = 'job-ad-container mb-4';
const inactiveAdTitle = document.createElement('p');
inactiveAdTitle.className = 'job-ad-title text-gray-800';
inactiveAdTitle.textContent = `Extra job ad #${i + activeCount}`;
const inactiveAd = document.createElement('div');
inactiveAd.className = 'job-ad-detail inactive-job-ad p-4 bg-white rounded-lg shadow';
inactiveAd.innerHTML = `
<p class="text-gray-500 status">Ready to be used / Not active</p>
`;
inactiveAdContainer.appendChild(inactiveAdTitle);
inactiveAdContainer.appendChild(inactiveAd);
container.appendChild(inactiveAdContainer);
}
}
function showExtraJobAdsTab() {
document.getElementById('extra-job-ads-content').classList.remove('hidden');
document.getElementById('extra-job-ads-tab').classList.add('custom-tab-active');
document.getElementById('extra-job-ads-tab').classList.remove('custom-tab-inactive');
loadComponent('buttons-container', 'components/Button.html', () => {
document.getElementById('button-text').innerText = 'GET EXTRA JOB ADS';
const buttonElement = document.getElementById('button-element');
buttonElement.classList.add('bg-black', 'text-white');
buttonElement.innerHTML += `
<svg class="h-6 w-6 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
`;
});
// Generate job ads dynamically
generateJobAds(2, 1);
}

View File

@@ -0,0 +1,7 @@
<div id="extra-job-ads-content" class="p-4 bg-white rounded-lg shadow mb-4">
<h3 class="text-xl font-bold mb-4">You don't have any extra job ads at the moment lorem ipsum</h3>
<p class="text-gray-600 mb-4">Generic description talking about the benefits of extra job ads lorem ipsum</p>
<div class="w-full h-48 bg-gray-200 flex items-center justify-center rounded-lg mb-4">
<p class="text-gray-500">empty state illustration</p>
</div>
</div>

View File

@@ -0,0 +1,42 @@
.no-subscription-container {
width: 100%;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.no-subscription-container h3 {
color: var(--app-text-stroke-text-stroke-day, #18191E);
text-align: center;
font-family: 'Montserrat', sans-serif;
font-size: 24px;
font-style: normal;
font-weight: 700;
line-height: 32px;
margin-bottom: 16px;
}
.no-subscription-container p {
color: var(--app-text-stroke-text-stroke-day, #18191E);
text-align: center;
font-family: 'Inter', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px;
margin-bottom: 16px;
}
.no-subscription-illustration {
width: var(--Chart-Module-Width, 325px);
height: 260px;
background-color: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
margin: 0 auto;
}

View File

@@ -0,0 +1,7 @@
<div id="no-subscription-content" class="no-subscription-container">
<h3 id="no-subscription-title">You don't have any extra job ads at the moment lorem ipsum</h3>
<p id="no-subscription-description">Generic description talking about the benefits of extra job ads lorem pisum</p>
<div class="no-subscription-illustration">
<img id="no-subscription-image" src="" alt="Illustration" class="w-full h-full object-cover">
</div>
</div>

View File

@@ -0,0 +1,9 @@
body {
font-family: var(--font-family-inter);
}
.content-container {
background-color: #f3f4f6;
padding-bottom: 4rem;
}

View File

@@ -0,0 +1,4 @@
<div id="plan-header" class="mb-4">
<h2 class="text-xl font-semibold text-gray-900">Your plan: <span class="text-gray-600">&lt;Plan name from stripe&gt;</span></h2>
</div>
<div id="plan-detail-items" class="space-y-4 bg-white p-6 rounded-lg shadow"></div>

View File

@@ -0,0 +1,6 @@
<!-- PlanDetailItem.html -->
<div class="mb-4 p-4 bg-gray-50 rounded-lg shadow">
<p class="text-gray-500">{{label}}</p>
<p class="text-gray-800">{{value}}</p>
</div>

View File

@@ -0,0 +1,36 @@
#plan-detail-items {
font-family: 'Inter', sans-serif;
}
.plan-detail-item {
display: flex;
flex-direction: column;
width: 345px;
padding: 0px 8px 9px 0px;
align-items: flex-start;
gap: 2px;
margin-bottom: 9px;
border-bottom: 1px solid #e5e7eb;
}
.plan-detail-item:last-child {
border-bottom: none;
}
.plan-detail-label {
color: var(--app-text-stroke-text-stroke-day, #18191E);
font-family: 'Inter', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
}
.plan-detail-value {
color: var(--app-text-stroke-text-stroke-day, #18191E);
font-family: 'Inter', sans-serif;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}

View File

@@ -0,0 +1,22 @@
<div id="subscriptions-content" class="p-4 bg-white rounded-lg shadow mb-4">
<h3 class="text-xl font-bold mb-4">Your plan: <Plan name from stripe></h3>
<!-- Description -->
<div id="plan-detail-description"></div>
<!-- Price per month -->
<div id="plan-detail-price"></div>
<!-- Active job ads -->
<div id="plan-detail-ads"></div>
<!-- Renewal date -->
<div id="plan-detail-renewal"></div>
</div>
<!-- Action Buttons for Subscriptions Tab -->
<div id="subscriptions-buttons" class="sticky-footer">
<div id="update-plan-button"></div>
<a href="#" class="block text-center text-red-600" id="cancel-subscription-button">Cancel subscription</a>
</div>

View File

@@ -0,0 +1,56 @@
function showSubscriptionTab(planCanceled) {
document.getElementById('subscriptions-content').classList.remove('hidden');
document.getElementById('subscriptions-tab').classList.add('custom-tab-active');
document.getElementById('subscriptions-tab').classList.remove('custom-tab-inactive');
loadComponent('buttons-container', 'components/Button.html', () => {
const buttonText = planCanceled ? 'RE-SUBSCRIBE' : 'UPDATE PLAN';
document.getElementById('button-text').innerText = buttonText;
const buttonElement = document.getElementById('button-element');
buttonElement.classList.add('bg-black', 'text-white');
buttonElement.innerHTML += `
<svg class="h-6 w-6 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
`;
if (!planCanceled) {
const buttonsContainer = document.getElementById('buttons-container');
buttonsContainer.innerHTML += '<a href="#" class="block text-center text-red-600">Cancel subscription</a>';
}
});
// Plan details
const planDetails = [
{ label: 'Description', value: 'Plan description from stripe lorem ipsum', className: 'text-gray-800', labelClassName: 'text-gray-500' },
{ label: 'Price per month', value: 'XX.XX € / month', className: 'text-gray-800', labelClassName: 'text-gray-500' },
{ label: 'Active job ads', value: 'X / Total N', className: planCanceled ? 'text-red-500' : 'text-gray-800', labelClassName: planCanceled ? 'text-red-500' : 'text-gray-500' },
{ label: planCanceled ? 'Expiry date' : 'Renewal date', value: 'DD month YYYY', additionalText: planCanceled ? '(X days remaining)' : '', className: planCanceled ? 'text-red-500' : 'text-gray-800', labelClassName: planCanceled ? 'text-red-500' : 'text-gray-500' },
];
const planDetailItemsContainer = document.getElementById('plan-detail-items');
planDetailItemsContainer.innerHTML = '';
planDetails.forEach(detail => {
loadComponent(null, 'components/PlanDetailItem.html', () => {
const item = document.createElement('div');
item.className = 'plan-detail-item';
item.innerHTML = `
<p class="${detail.labelClassName}">${detail.label}</p>
<p class="${detail.className}">${detail.value} ${detail.additionalText || ''}</p>
`;
planDetailItemsContainer.appendChild(item);
});
});
if (planCanceled) {
const attentionMessageContainer = document.createElement('div');
attentionMessageContainer.id = 'attention-message-container';
document.getElementById('subscriptions-content').appendChild(attentionMessageContainer);
loadComponent('attention-message-container', 'components/AttentionMessage.html');
} else {
const attentionMessageContainer = document.getElementById('attention-message-container');
if (attentionMessageContainer) {
attentionMessageContainer.innerHTML = '';
}
}
}

View File

@@ -0,0 +1,4 @@
<div class="custom-tabs">
<div id="subscriptions-tab" class="custom-tab custom-tab-active" onclick="showTab('subscriptions')">Subscriptions</div>
<div id="extra-job-ads-tab" class="custom-tab custom-tab-inactive" onclick="showTab('extra-job-ads')">Extra job ads (0)</div>
</div>

View File

@@ -0,0 +1,48 @@
function loadComponent(containerId, filePath, callback) {
fetch(filePath)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(data => {
if (containerId) {
document.getElementById(containerId).innerHTML = data;
}
if (callback) callback();
})
.catch(error => console.error('Error loading component:', error));
}
function showTab(tab) {
document.getElementById('subscriptions-content').classList.add('hidden');
document.getElementById('extra-job-ads-content').classList.add('hidden');
document.getElementById('subscriptions-tab').classList.remove('custom-tab-active');
document.getElementById('extra-job-ads-tab').classList.remove('custom-tab-active');
document.getElementById('subscriptions-tab').classList.add('custom-tab-inactive');
document.getElementById('extra-job-ads-tab').classList.add('custom-tab-inactive');
document.getElementById('buttons-container').innerHTML = '';
if (tab === 'subscriptions') {
const planCanceled = false; // Set true or false
showSubscriptionTab(planCanceled);
} else if (tab === 'extra-job-ads') {
showExtraJobAdsTab();
}
}
document.addEventListener("DOMContentLoaded", () => {
loadComponent('header-container', 'components/Header.html');
loadComponent('tabs-container', 'components/Tabs.html');
loadComponent('subscriptions-content', 'components/PlanDetail.html');
loadComponent('extra-job-ads-content', 'components/ExtraJobAds.html', () => {
generateJobAds(2, 1); //Active-Inactive Jobads
});
loadComponent('buttons-container', 'components/Button.html', () => {
showTab('subscriptions');
});
});

View File

@@ -0,0 +1,71 @@
function loadComponent(containerId, filePath, callback) {
fetch(filePath)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(data => {
if (containerId) {
document.getElementById(containerId).innerHTML = data;
}
if (callback) callback();
})
.catch(error => console.error('Error loading component:', error));
}
function showTab(tabsConfig, activeTab) {
// Hide all content sections and reset tab classes
tabsConfig.forEach(tab => {
document.getElementById(tab.contentId).classList.add('hidden');
document.getElementById(tab.tabId).classList.remove('custom-tab-active');
document.getElementById(tab.tabId).classList.add('custom-tab-inactive');
});
// Show the selected content section and activate the tab
const activeConfig = tabsConfig.find(tab => tab.name === activeTab);
document.getElementById(activeConfig.contentId).classList.remove('hidden');
document.getElementById(activeConfig.tabId).classList.add('custom-tab-active');
document.getElementById(activeConfig.tabId).classList.remove('custom-tab-inactive');
// Load buttons for the active tab
document.getElementById('buttons-container').innerHTML = '';
loadComponent('buttons-container', activeConfig.buttonFile, () => {
document.getElementById('button-text').innerText = activeConfig.buttonText;
const buttonElement = document.getElementById('button-element');
buttonElement.classList.add('bg-black', 'text-white');
buttonElement.innerHTML += activeConfig.buttonIcon;
});
// Update content based on the active tab
if (activeTab === 'subscriptions') {
updateNoSubscriptionContent('You are not subscribed to any plan lorem ipsum dolor sit amet', 'Generic description talking about the benefits of subscribing lorem ipsum', 'path/to/subscription-illustration.png');
} else if (activeTab === 'extra-job-ads') {
updateNoSubscriptionContent('You don\'t have any extra job ads at the moment lorem ipsum', 'Generic description talking about the benefits of extra job ads lorem pisum', 'path/to/extra-job-ads-illustration.png');
}
}
function initializePage(tabsConfig) {
document.addEventListener("DOMContentLoaded", () => {
loadComponent('header-container', 'components/Header.html');
loadComponent('tabs-container', 'components/Tabs.html', () => {
tabsConfig.forEach(tab => {
document.getElementById(tab.tabId).addEventListener('click', () => showTab(tabsConfig, tab.name));
});
});
tabsConfig.forEach(tab => {
loadComponent(tab.contentId, tab.contentFile);
});
loadComponent('buttons-container', 'components/Button.html', () => {
// Show the first tab by default
showTab(tabsConfig, tabsConfig[0].name);
});
});
}
function updateNoSubscriptionContent(title, description, imagePath) {
document.getElementById('no-subscription-title').innerText = title;
document.getElementById('no-subscription-description').innerText = description;
document.getElementById('no-subscription-image').src = imagePath;
}

View File

@@ -0,0 +1,4 @@
<p id="caption" class="mt-2 text-sm text-gray-500">
This is a descriptive caption for the media.
</p>

View File

@@ -0,0 +1,46 @@
.carousel {
display: flex;
gap: 16px;
overflow-x: scroll;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
padding: 10px;
}
.carousel-item {
width: 420px;
height: 544px;
flex-shrink: 0;
scroll-snap-align: start;
display: flex;
flex-direction: column;
}
.carousel-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.carousel-item .caption {
margin-top: 8px;
text-align: center;
font-size: 14px;
color: #555;
}
@media (max-width: 640px) {
.carousel {
gap: 8px;
padding: 8px;
}
.carousel-item {
width: 250px;
height: 320px;
}
.carousel-item .caption {
font-size: 12px;
}
}

View File

@@ -0,0 +1,33 @@
document.addEventListener("DOMContentLoaded", function () {
const carouselContainer = document.getElementById('carousel-container');
const mediaItems = Array(10).fill({
type: 'image',
src: 'https://s3-alpha-sig.figma.com/img/37a7/42fb/1daabd5424c6093ca79a6d502b3b84ef?Expires=1724630400&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=FgQK8BhmZm52syueYHL8d33N9v48-lePOhb~KQgE528f-GZn8a8HXap5OvWlCdKaf2nZLlFvmgWgpnuz7iekJqEAS95CknKbniYODvBDOGMVIcHrU7YthPZ~YqZSE7pEANVBVEwkB9-1zJ77gT9uEMryjd-xb44NjnBhfLPkP4B9qlqkbuehRRhLGPBnYA9q3PHpf5ocx7j0~xAbomT~EFX2bzwBu70gKN0qTFVRy8uNu8USYah2YotQH58ChxcEPokhPxENAdNCWeDodsWVFrldbeU0CgVzCrn3MLYu9Ep96r7tjAvKfgQQqC6eORHyM-citVFu1DlJx8-CUN26RQ__',
alt: 'Image with Caption'
});
const renderCarousel = () => {
mediaItems.forEach((item, index) => {
const halfWidthClass = index === 2 ? 'half-width' : '';
const captionText = `Optional image caption lorem ipsum dolor sit amet, consectetur adipiscing elit lorem ipsum`;
carouselContainer.innerHTML += `
<div class="carousel-item ${halfWidthClass}">
${renderMedia(item)}
<div class="caption">${captionText}</div>
</div>
`;
});
};
const renderMedia = (item) => {
if (item.type === 'image') {
return `<img src="${item.src}" alt="${item.alt}">`;
} else if (item.type === 'video') {
return `<video src="${item.src}" controls class="w-full h-auto"></video>`;
}
return '';
};
renderCarousel();
});

View File

@@ -0,0 +1,77 @@
.checkbox-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
width: 130px;
}
.checkbox-input {
appearance: none;
border: 2px solid #0DC3FF;
border-radius: 4px;
width: 87px;
height: 20px;
display: flex;
align-items: flex-start;
gap: var(--Input-Element-Radius-CTA-M, 12px);
cursor: pointer;
position: relative;
flex-shrink: 0;
transition: width 0.3s ease, height 0.3s ease;
}
.checkbox-input:checked {
background-color: #0DC3FF;
width: var(--Icon-Sizes-Input, 20px);
height: var(--Icon-Sizes-Input, 20px);
flex-shrink: 0;
}
.checkbox-input:checked::before {
content: '✔';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
flex: 1 0 0;
color: var(--app-input-text-label-day, #1E2024);
font-family: Inter, sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
}
.checkbox-icon {
margin-right: 0.5rem;
}
.checkbox-label-left {
display: flex;
align-items: center;
justify-content: flex-start;
}
.checkbox-label-left .checkbox-input {
order: 1;
margin-right: auto;
}
.checkbox-label-right {
display: flex;
align-items: center;
justify-content: flex-end;
}
.checkbox-label-right .checkbox-input {
order: 2;
margin-left: auto;
}

View File

@@ -0,0 +1,39 @@
<div class="checkbox-component">
<!-- Horizontal Checkbox with Label and Icon -->
<div class="checkbox-container">
<input type="checkbox" id="checkbox1" class="checkbox checkbox-primary">
<label for="checkbox1" class="checkbox-label">Label with Icon</label>
<span class="checkbox-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11.41l-3.3 3.29-1.29-1.29a1 1 0 10-1.42 1.42l2 2a1 1 0 001.42 0l4-4a1 1 0 00-1.42-1.42z" clip-rule="evenodd" />
</svg>
</span>
</div>
<!-- Horizontal Checkbox with Label and No Icon -->
<div class="checkbox-container">
<input type="checkbox" id="checkbox2" class="checkbox checkbox-primary">
<label for="checkbox2" class="checkbox-label">Label without Icon</label>
</div>
<!-- Horizontal Checkbox with No Label and Icon -->
<div class="checkbox-container">
<input type="checkbox" id="checkbox3" class="checkbox checkbox-primary">
<span class="checkbox-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11.41l-3.3 3.29-1.29-1.29a1 1 0 10-1.42 1.42l2 2a1 1 0 001.42 0l4-4a1 1 0 00-1.42-1.42z" clip-rule="evenodd" />
</svg>
</span>
</div>
<!-- Vertical Checkbox with Label and Icon -->
<div class="checkbox-container flex-col">
<input type="checkbox" id="checkbox4" class="checkbox checkbox-primary">
<label for="checkbox4" class="checkbox-label">Vertical Label with Icon</label>
<span class="checkbox-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11.41l-3.3 3.29-1.29-1.29a1 1 0 10-1.42 1.42l2 2a1 1 0 001.42 0l4-4a1 1 0 00-1.42-1.42z" clip-rule="evenodd" />
</svg>
</span>
</div>
</div>

View File

@@ -0,0 +1,51 @@
const checkboxData = [
{ label: 'Option 1', position: 'right' },
{ label: 'Option 2', icon: '📞', position: 'right' },
{ label: 'Option 3', icon: '📞', position: 'left' },
{ label: 'Option 4', icon: '📞', position: 'left' },
];
function createCheckboxComponent({ label, icon, position, orientation }) {
const checkboxContainer = document.createElement('div');
checkboxContainer.className = `checkbox-container ${orientation === 'vertical' ? 'flex-col' : ''}`;
const checkboxInput = document.createElement('input');
checkboxInput.type = 'checkbox';
checkboxInput.className = 'checkbox checkbox-primary';
checkboxInput.id = label;
const checkboxLabel = document.createElement('label');
checkboxLabel.className = `checkbox-label checkbox-label-${position}`;
checkboxLabel.htmlFor = label;
if (icon) {
const iconElement = document.createElement('span');
iconElement.className = 'icon';
iconElement.innerText = icon;
checkboxLabel.appendChild(iconElement);
}
if (label) {
const labelText = document.createElement('span');
labelText.innerText = label;
checkboxLabel.appendChild(labelText);
}
if (position === 'left' || orientation === 'vertical') {
checkboxContainer.appendChild(checkboxLabel);
checkboxContainer.appendChild(checkboxInput);
} else {
checkboxContainer.appendChild(checkboxInput);
checkboxContainer.appendChild(checkboxLabel);
}
return checkboxContainer;
}
document.addEventListener('DOMContentLoaded', () => {
const checkboxContainer = document.getElementById('checkbox-container');
checkboxData.forEach(checkboxConfig => {
const checkboxComponent = createCheckboxComponent(checkboxConfig);
checkboxContainer.appendChild(checkboxComponent);
});
});

View File

@@ -0,0 +1,41 @@
body {
font-family: 'Inter', sans-serif;
background-color: var(--_app-global-grayscale-100, #F2F4F7);
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.content-container {
flex: 1;
padding-bottom: 4rem;
background-color: var(--_app-global-grayscale-100, #F2F4F7);
}
.sticky-footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 1rem;
background-color: #fff;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}
.header-container {
width: 100%;
background-color: var(--_app-global-grayscale-100, #F2F4F7);
padding: 1rem;
box-shadow: none;
}
#plan-header {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: #18191E;
}

View File

@@ -0,0 +1,114 @@
.dropdown-container {
width: var(--Chart-Module-Width, 325px);
margin-bottom: 1rem;
position: relative;
}
.dropdown-label {
display: flex;
height: var(--Icon-Sizes-Input, 20px);
flex-direction: column;
justify-content: flex-end;
flex: 1 0 0;
color: var(--app-input-text-label-day, #1E2024);
font-family: Inter, sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 20px;
margin-bottom: 0.5rem;
}
.dropdown-description {
align-self: stretch;
color: var(--app-input-text-label-light-day, #344054);
font-family: Inter, sans-serif;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 16px;
margin-top: 0.5rem;
}
.dropdown-placeholder {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
flex: 1 0 0;
overflow: hidden;
color: var(--app-input-text-placeholder-text-day, #7D8188);
text-overflow: ellipsis;
font-family: Inter, sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
}
.dropdown-button {
width: var(--Chart-Module-Width, 325px);
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem;
border: 1px solid var(--app-input-element-stroke-day, #98A2B3);
border-radius: 4px;
background: var(--app-input-element-background-day, #F9FAFB);
cursor: pointer;
}
.dropdown-list {
display: none;
flex-direction: column;
align-items: flex-start;
width: var(--Chart-Module-Width, 325px);
max-height: 200px;
overflow-y: auto;
padding: var(--Input-Element-Radius-CTA-S, 6px) 2px var(--Spacing-Keywords, 0px) 2px;
border: 1px solid var(--app-input-element-stroke-day, #98A2B3);
border-radius: 0 0 4px 4px;
background: var(--app-input-element-background-day, #F9FAFB);
position: absolute;
left: 0;
z-index: 10;
}
.dropdown-list-with-description {
top: 75%;
}
.dropdown-list-no-description {
top: 100%;
}
.dropdown-item {
display: flex;
height: 40px;
padding: 8px var(--Icon-Sizes-Input, 20px) 8px 8px;
justify-content: space-between;
align-items: flex-start;
align-self: stretch;
cursor: pointer;
transition: background-color 0.3s, color 0.3s;
}
.dropdown-item:hover {
background-color: #0DC3FF;
}
.dropdown-item-text {
flex: 1 0 0;
color: var(--app-input-text-user-input-day, #1E2024);
font-family: Inter, sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
transition: color 0.3s;
}
.dropdown-item:hover .dropdown-item-text {
color: var(--app-input-text-user-input-day, #1E2024);
font-weight: 600;
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dropdown Component Example</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/daisyui@1.15.0/dist/full.css" rel="stylesheet">
<link rel="stylesheet" href="dropdown.css">
</head>
<body class="bg-gray-100 p-10">
<h1 class="text-2xl font-bold mb-4">Dropdown Component Example</h1>
<div id="dropdown-container"></div>
<script>
console.log("Loading dropdown.js...");
</script>
<script src="dropdown.js"></script>
</body>
</html>

View File

@@ -0,0 +1,87 @@
const dropdownData = [
{ label: 'Options', placeholder: 'Select an option', description: 'Choose one of the following items', items: ['Item 1', 'Item 2', 'Item 3'] },
{ placeholder: 'Select without label', items: ['Item A', 'Item B', 'Item C', 'Item D', 'Item E', 'Item F', 'Item G', 'Item H', 'Item I', 'Item J', 'Item K'] },
{ label: 'Options 2', placeholder: 'Choose an item', description: 'Additional description lorem ipsum', items: ['Option 1', 'Option 2', 'Option 3'] },
];
function createDropdownComponent({ label, placeholder, description, items }) {
const dropdownContainer = document.createElement('div');
dropdownContainer.className = 'dropdown-container mb-6';
if (label) {
const labelElement = document.createElement('label');
labelElement.className = 'dropdown-label block text-gray-700 text-sm font-bold mb-2';
labelElement.innerText = label;
dropdownContainer.appendChild(labelElement);
}
const buttonElement = document.createElement('div');
buttonElement.className = 'dropdown-button';
buttonElement.innerText = placeholder || 'Select an option';
dropdownContainer.appendChild(buttonElement);
const dropdownList = document.createElement('div');
dropdownList.className = 'dropdown-list';
dropdownList.style.display = 'none';
if (description) {
dropdownList.classList.add('dropdown-list-with-description');
} else {
dropdownList.classList.add('dropdown-list-no-description');
}
items.forEach(item => {
const itemElement = document.createElement('div');
itemElement.className = 'dropdown-item';
const itemText = document.createElement('span');
itemText.className = 'dropdown-item-text';
itemText.innerText = item;
itemElement.appendChild(itemText);
itemElement.addEventListener('click', () => {
buttonElement.innerHTML = '';
buttonElement.className = 'dropdown-button selected-dropdown-item';
const selectedItemText = document.createElement('span');
selectedItemText.className = 'selected-dropdown-item-text';
selectedItemText.innerText = item;
buttonElement.appendChild(selectedItemText);
dropdownList.style.display = 'none';
});
dropdownList.appendChild(itemElement);
});
buttonElement.addEventListener('click', (event) => {
event.stopPropagation();
dropdownList.style.display = dropdownList.style.display === 'none' ? 'flex' : 'none';
});
document.addEventListener('click', (event) => {
if (!dropdownContainer.contains(event.target)) {
dropdownList.style.display = 'none';
}
});
dropdownContainer.appendChild(dropdownList);
if (description) {
const descriptionElement = document.createElement('p');
descriptionElement.className = 'dropdown-description text-gray-600 text-xs italic mt-2';
descriptionElement.innerText = description;
dropdownContainer.appendChild(descriptionElement);
}
return dropdownContainer;
}
document.addEventListener('DOMContentLoaded', () => {
console.log("Document loaded, rendering dropdowns...");
const dropdownContainer = document.getElementById('dropdown-container');
dropdownData.forEach(dropdownConfig => {
const dropdownComponent = createDropdownComponent(dropdownConfig);
dropdownContainer.appendChild(dropdownComponent);
});
});

View File

@@ -0,0 +1,43 @@
.duo-layout {
display: flex;
flex-direction: row;
gap: 16px;
justify-content: center;
padding: 0 16px;
}
.duo-layout .media-item {
width: 500px;
height: 720px;
flex-shrink: 0;
overflow: hidden;
}
.duo-layout .media-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
@media (max-width: 640px) {
.duo-layout {
justify-content: space-between;
gap: 8px;
padding: 0 16px;
max-width: 100%;
}
.duo-layout .media-item {
width: calc(50% - 5px);
max-width: 200px;
height: 45vh;
margin: 0;
}
.duo-layout .media-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
}

View File

@@ -0,0 +1,10 @@
.sticky-footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 1rem;
background-color: #fff;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}

View File

@@ -0,0 +1,285 @@
body {
margin: 0;
font-family: 'Montserrat', sans-serif;
position: relative;
}
.header-background {
height: 80vh;
background-size: cover;
background-position: center;
position: relative;
z-index: 1;
}
.remaining-background {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 20vh;
background-color: black;
z-index: 1;
}
.container {
width: 78%;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 3;
}
.logo {
width: 160px;
height: auto;
flex-shrink: 0;
margin-left: 0;
}
.nav-link {
color: #f6f6f6;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 800;
line-height: normal;
letter-spacing: -0.32px;
margin-right: 1.5vw;
}
.menu-button {
display: none;
background: none;
border: none;
cursor: pointer;
}
.menu-icon {
width: 30px;
height: auto;
}
.text-box {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5vh;
width: 30vw;
height: 35vh;
padding: 3vh 3.5vw;
border-radius: 0.5vh;
background-color: #161616;
color: #f6f6f6;
margin-bottom: 1vh;
transform: translateY(-5vh);
}
.headline1 {
color: #f6f6f6;
font-family: 'Montserrat', sans-serif;
font-size: 36px;
font-style: normal;
font-weight: 800;
line-height: 44px;
letter-spacing: -0.72px;
margin: 0;
}
.headline2 {
font-family: 'Montserrat', sans-serif;
font-size: 36px;
font-style: normal;
font-weight: 800;
line-height: 44px;
letter-spacing: -0.72px;
background: linear-gradient(104deg, #00FFC2 0%, #00C0FF 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
margin: 0;
}
.description-text {
font-family: 'Montserrat', sans-serif;
font-size: 22px;
font-style: normal;
font-weight: 600;
line-height: 32px;
color: #f6f6f6;
margin-top: 2vh;
max-width: 100%;
}
.text-highlight {
background: linear-gradient(103deg, #00FFC2 15.48%, #00C0FF 39.9%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-family: 'Montserrat', sans-serif;
font-size: 22px;
font-style: normal;
font-weight: 700;
line-height: 25px;
letter-spacing: -0.44px;
}
.email-signup-box {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1vh;
width: 30vw;
padding: 2vh 3.5vw;
border-radius: 0.5vh;
background-color: #292F38;
color: #f6f6f6;
box-shadow: 0px 0.5vh 1.5vh rgba(0, 0, 0, 0.25);
transform: translateY(-7vh);
}
.email-input {
width: 284px;
height: 48px;
border-radius: 4px;
border: 1px solid #667085;
background-color: #161616;
color: #ffffff;
padding: 0.5vh 1vw;
}
.gradient-button {
background: linear-gradient(90deg, #00ffc2 0%, #00c0ff 100%);
color: white;
padding: 1vh 2vw;
border-radius: 0.4vh;
border: none;
cursor: pointer;
transition: background 0.3s ease;
}
.header-gradient {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200vh;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0) 80%);
z-index: 2;
pointer-events: none;
}
@media only screen and (max-width: 768px) {
.header-container {
background-size: 250%;
background-position: center top;
flex-direction: column;
padding: 2vh 5vw;
}
.header-background {
background-size: cover;
background-position: center;
height: 80vh;
background-color: black;
}
.container {
width: auto;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.logo {
width: 180px;
height: auto;
margin-top: 40px;
margin-left: 45px;
}
.nav-link {
display: none;
}
.menu-button {
display: flex;
align-items: center;
margin-right: 40px;
margin-top: 45px;
}
.menu-icon {
display: block;
width: 33px;
height: auto;
}
.text-box, .email-signup-box {
width: 80vw;
margin-left: -7vw;
margin-right: 0;
text-align: left;
transform: translateY(3vh);
}
.text-box {
padding: 3vh 4vw;
height: auto;
}
.email-signup-box {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 20px;
background-color: #292F38;
color: #f6f6f6;
box-sizing: border-box;
border-radius: 8px;
height: auto;
min-height: 210px;
transform: translateY(1vh);
}
.email-input {
width: 87.5%;
height: 48px;
padding: 12px;
border-radius: 4px;
border: 1px solid #667084;
background-color: #161616;
color: #f6f6f6;
margin: 0;
margin-bottom: 10px;
box-sizing: border-box;
position: absolute;
top: 76px;
left: 20px;
}
.gradient-button {
width: 87.5%;
height: 48px;
padding: 12px;
background: linear-gradient(90deg, #00ffc2 0%, #00c0ff 100%);
color: #ffffff;
text-align: center;
cursor: pointer;
border: none;
border-radius: 16px;
box-sizing: border-box;
position: absolute;
top: 140px;
left: 13px;
}
.gradient-button:hover {
background: linear-gradient(90deg, #00c0ff 0%, #00ffc2 100%);
}
}

View File

@@ -0,0 +1,66 @@
<!-- components/header.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Tailwind CSS CDN -->
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<!-- DaisyUI CDN -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@2.15.3/dist/full.css" rel="stylesheet">
<!-- Link to the CSS file -->
<link rel="stylesheet" href="components/header.css">
<title>Header Component</title>
</head>
<body>
<header class="relative w-full bg-cover bg-center header-background" style="height: 100vh; background-image: url('https://s3-alpha-sig.figma.com/img/193d/32f1/73586125aba9fb9700efa4c194bb4fd2?Expires=1725840000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=XPsu3f44VaAU9oU0vw4rWpKWYIEyYYXX3IKJLZGipBD0FqufIBt6fB5Ng3kuY~hx~TWh5iNa4vggL5IwCXf6rLyyZWLv6dydPIdGKzM3dSiVnVwQA5xwtJ5rXvxUMBbI9kXJRxq96~byLWCPjOPNWtbibTG7ptbcs98bkBFF8TBSPskgvx~XNorIq1Q2P2uaxMe25m7LQ89wFXsVSbWDn75j7Gkvyutu2vAiz2E9bxG7~o3naMwz1fpec8tSkgN9XGqnXKruYYyzJ2pDc4tz3PC80vVncK1-Oh-MPXVdlLQPQjd8wM1CmNWGuz2FN7Q7KCF-bXeRl8eq-jXoeNjRmw__');">
<!-- Gradient overlay -->
<div class="header-gradient"></div>
<div class="absolute inset-0 bg-black opacity-20"></div>
<!-- Black background for the remaining 20% -->
<div class="remaining-background"></div>
<div class="relative z-10 flex items-center justify-between p-4 container">
<!-- Logo -->
<div class="flex items-center">
<img src='./static/img/icons/Smatchit_Logo.png' class="logo" alt="Logo">
</div>
<!-- Navigation Links (Visible in Desktop Mode) -->
<nav class="space-x-8 text-white nav-links">
<a href="#" class="nav-link">Pour les entreprises</a>
<a href="#" class="nav-link">Contact</a>
</nav>
<!-- Menu Button (Visible in Mobile Mode) -->
<button class="menu-button">
<img src='./static/img/icons/menu.png' class="menu-icon" alt="Menu">
</button>
</div>
<!-- Main Content -->
<div class="relative z-20 flex flex-col items-start justify-center h-full text-left text-white px-8" style="padding-top: 4vh; margin-left: 10vw;">
<!-- Text Box -->
<div class="text-box">
<p class="headline1">Votre</p>
<p class="headline2">agent de recrutement</p>
<p class="headline1">personnel 24/7.</p>
<p class="description-text">
Laissez <span class="text-highlight">smatchit</span> faire pour trouver votre job, détendez-vous et soyez à lheure à vos futurs entretiens 😊
</p>
</div>
<!-- Email Signup Box -->
<div class="email-signup-box mt-4">
<p class="mb-2">Soyez alerté du lancement de <span class="text-highlight">smatchit</span> :</p>
<div class="flex items-center w-full space-x-2">
<input type="email" placeholder="Enter your e-mail" class="email-input" />
<button type="submit" class="gradient-button">SEND</button>
</div>
</div>
</div>
</header>
</body>
</html>

View File

@@ -0,0 +1,13 @@
// components/header/header.js
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('.btn-primary');
button.addEventListener('click', () => {
const emailInput = document.querySelector('.input[type="email"]').value;
if (emailInput) {
alert(`Email ${emailInput} submitted successfully!`);
} else {
alert('Please enter a valid email.');
}
});
});

View File

@@ -0,0 +1,8 @@
.inset-wrapper {
max-width: 1024px;
margin: 0 auto;
box-shadow: none;
border-radius: 0;
padding: 0;
background-color: transparent;
}

View File

@@ -0,0 +1,4 @@
<!-- media-item.html -->
<div class="media-item">
<img src="{{src}}" alt="{{alt}}" class="w-full h-auto object-cover">
</div>

View File

@@ -0,0 +1,72 @@
// layout-manager.js
document.addEventListener("DOMContentLoaded", function () {
const mediaContainer = document.getElementById('media-container');
// Media items with the specified image URL repeated
const mediaItems = [
{ type: 'image', src: 'https://s3-alpha-sig.figma.com/img/37a7/42fb/1daabd5424c6093ca79a6d502b3b84ef?Expires=1724630400&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=FgQK8BhmZm52syueYHL8d33N9v48-lePOhb~KQgE528f-GZn8a8HXap5OvWlCdKaf2nZLlFvmgWgpnuz7iekJqEAS95CknKbniYODvBDOGMVIcHrU7YthPZ~YqZSE7pEANVBVEwkB9-1zJ77gT9uEMryjd-xb44NjnBhfLPkP4B9qlqkbuehRRhLGPBnYA9q3PHpf5ocx7j0~xAbomT~EFX2bzwBu70gKN0qTFVRy8uNu8USYah2YotQH58ChxcEPokhPxENAdNCWeDodsWVFrldbeU0CgVzCrn3MLYu9Ep96r7tjAvKfgQQqC6eORHyM-citVFu1DlJx8-CUN26RQ__', alt: 'Image 1' },
{ type: 'image', src: 'https://s3-alpha-sig.figma.com/img/37a7/42fb/1daabd5424c6093ca79a6d502b3b84ef?Expires=1724630400&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=FgQK8BhmZm52syueYHL8d33N9v48-lePOhb~KQgE528f-GZn8a8HXap5OvWlCdKaf2nZLlFvmgWgpnuz7iekJqEAS95CknKbniYODvBDOGMVIcHrU7YthPZ~YqZSE7pEANVBVEwkB9-1zJ77gT9uEMryjd-xb44NjnBhfLPkP4B9qlqkbuehRRhLGPBnYA9q3PHpf5ocx7j0~xAbomT~EFX2bzwBu70gKN0qTFVRy8uNu8USYah2YotQH58ChxcEPokhPxENAdNCWeDodsWVFrldbeU0CgVzCrn3MLYu9Ep96r7tjAvKfgQQqC6eORHyM-citVFu1DlJx8-CUN26RQ__', alt: 'Image 2' },
// { type: 'image', src: 'https://s3-alpha-sig.figma.com/img/37a7/42fb/1daabd5424c6093ca79a6d502b3b84ef?Expires=1724630400&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=FgQK8BhmZm52syueYHL8d33N9v48-lePOhb~KQgE528f-GZn8a8HXap5OvWlCdKaf2nZLlFvmgWgpnuz7iekJqEAS95CknKbniYODvBDOGMVIcHrU7YthPZ~YqZSE7pEANVBVEwkB9-1zJ77gT9uEMryjd-xb44NjnBhfLPkP4B9qlqkbuehRRhLGPBnYA9q3PHpf5ocx7j0~xAbomT~EFX2bzwBu70gKN0qTFVRy8uNu8USYah2YotQH58ChxcEPokhPxENAdNCWeDodsWVFrldbeU0CgVzCrn3MLYu9Ep96r7tjAvKfgQQqC6eORHyM-citVFu1DlJx8-CUN26RQ__', alt: 'Image 3' },
{ type: 'image', src: 'https://s3-alpha-sig.figma.com/img/37a7/42fb/1daabd5424c6093ca79a6d502b3b84ef?Expires=1724630400&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=FgQK8BhmZm52syueYHL8d33N9v48-lePOhb~KQgE528f-GZn8a8HXap5OvWlCdKaf2nZLlFvmgWgpnuz7iekJqEAS95CknKbniYODvBDOGMVIcHrU7YthPZ~YqZSE7pEANVBVEwkB9-1zJ77gT9uEMryjd-xb44NjnBhfLPkP4B9qlqkbuehRRhLGPBnYA9q3PHpf5ocx7j0~xAbomT~EFX2bzwBu70gKN0qTFVRy8uNu8USYah2YotQH58ChxcEPokhPxENAdNCWeDodsWVFrldbeU0CgVzCrn3MLYu9Ep96r7tjAvKfgQQqC6eORHyM-citVFu1DlJx8-CUN26RQ__', alt: 'Image 4' },
];
const useQuatreA = false; // Set this to true for Quatre A, false for Quatre B
// Apply the appropriate layout based on the number of media items
const applyLayout = () => {
mediaContainer.innerHTML = ''; // Clear existing content
const itemCount = mediaItems.length;
if (itemCount === 1) {
// Single Image Layout
mediaContainer.className = 'single-layout';
mediaContainer.innerHTML += `<div class="media-item">${renderMedia(mediaItems[0])}</div>`;
} else if (itemCount === 2) {
// Duo Layout
mediaContainer.className = 'duo-layout';
mediaItems.forEach(item => {
mediaContainer.innerHTML += `<div class="media-item">${renderMedia(item)}</div>`;
});
} else if (itemCount === 3) {
// Trio Layout
mediaContainer.className = 'trio-layout';
mediaContainer.innerHTML += `<div class="left-column media-item">${renderMedia(mediaItems[0])}</div>`;
mediaContainer.innerHTML += `
<div class="right-column">
<div class="media-item">${renderMedia(mediaItems[1])}</div>
<div class="media-item">${renderMedia(mediaItems[2])}</div>
</div>`;
return;
} else if (itemCount >= 4) {
if (useQuatreA) {
// Quatre A Layout
mediaContainer.className = 'grid grid-cols-2 gap-4';
mediaItems.forEach(item => {
mediaContainer.innerHTML += `<div class="media-item">${renderMedia(item)}</div>`;
});
} else {
// Quatre B Layout
mediaContainer.className = 'quatre-b-layout';
mediaContainer.innerHTML += `<div class="top-image">${renderMedia(mediaItems[0])}</div>`;
mediaContainer.innerHTML += `<div class="bottom-row">`;
for (let i = 1; i < Math.min(5, itemCount); i++) {
mediaContainer.innerHTML += `<div class="media-item">${renderMedia(mediaItems[i])}</div>`;
}
mediaContainer.innerHTML += `</div>`;
}
}
};
// Function to render media based on its type
const renderMedia = (item) => {
if (item.type === 'image') {
return `<img src="${item.src}" alt="${item.alt}">`; /* Ensure images fill their containers */
} else if (item.type === 'video') {
return `<video src="${item.src}" controls class="w-full h-auto"></video>`;
}
return '';
};
// Apply the layout
applyLayout();
});

View File

@@ -0,0 +1,21 @@
document.addEventListener("DOMContentLoaded", function() {
const mediaType = 'image';
const hasPlayButton = true;
const image = document.getElementById('media-image');
const video = document.getElementById('media-video');
const playButton = document.getElementById('play-button');
const caption = document.getElementById('caption');
if (mediaType === 'image') {
image.classList.remove('hidden');
} else if (mediaType === 'video') {
video.classList.remove('hidden');
}
if (hasPlayButton && mediaType === 'video') {
playButton.classList.remove('hidden');
}
});

View File

@@ -0,0 +1,72 @@
.quatre-b-layout {
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.quatre-b-layout .top-image {
display: flex;
width: 100%;
height: 440px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
flex-shrink: 0;
grid-column: 1 / span 3;
}
.quatre-b-layout .top-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.quatre-b-layout .bottom-row {
display: contents;
}
.quatre-b-layout .media-item {
overflow: hidden;
grid-column: span 1;
width: 100%;
}
.quatre-b-layout .media-item img {
width: 100%;
height: auto;
object-fit: contain;
}
@media (max-width: 768px) {
.quatre-b-layout {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr;
gap: 10px;
}
.quatre-b-layout .top-image {
width: 100%;
height: auto;
grid-column: 1 / span 3;
}
.quatre-b-layout .bottom-row {
display: contents;
}
.quatre-b-layout .media-item {
width: 100%;
height: auto;
grid-column: span 1;
}
.quatre-b-layout .media-item img {
width: 100%;
height: auto;
object-fit: cover;
}
}

View File

@@ -0,0 +1,67 @@
.radio-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
width: 130px;
}
.radio-input {
appearance: none;
border: 2px solid #98A2B3;
border-radius: 50%;
width: var(--Chart-Column-Margin, 24px);
height: var(--Chart-Column-Margin, 24px);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
flex-shrink: 0;
position: relative;
}
.radio-input:checked {
border-color: #0DC3FF;
background-color: #0DC3FF;
}
.radio-input:checked::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 10px;
height: 10px;
background-color: white;
border-radius: 50%;
}
.radio-label {
display: flex;
align-items: center;
color: var(--app-input-text-label-day, #1E2024);
font-family: Inter, sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
}
.icon {
margin-right: 0.5rem;
}
.radio-label-left {
justify-content: flex-end;
margin-left: 10px;
order: 2;
flex-grow: 1;
}
.radio-label-right {
justify-content: flex-start;
margin-right: 10px;
order: 1;
flex-grow: 1;
}

Some files were not shown because too many files have changed in this diff Show More