class Link extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
cursor: pointer;
text-decoration: none;
}
`
}
render() {
return {
_: 'a',
children: this.props.children,
onclick: (event) => {
event.preventDefault();
let url = this.props.href;
route.go(url);
}
}
}
}class AboutSection extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self .content {
padding: 2rem 0;
width: 75%;
font-family: var(--font-js);
}
%self h1 {
font-size: 2.25rem;
font-weight: 400;
color: var(--text-color);
margin: 2rem 0;
}
%self p {
margin-top: 1rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5rem;
color: var(--text-color-light);
}
`
}
styles() {
return {
'md': /* css */`
%self .content {
width: 100%;
}
`
}
}
render() {
return {_: 'section', id: this.props.id, className: 'wrapper', children: [
_('div', {className: 'content'}, [
_('h1', this.props.title),
_('p', this.props.description),
])
]}
}
};
;
class AboutWhat extends AboutSection {
constructor() {
super({
id: 'what',
title: 'What is Ophose ?',
description: ['Ophose is a free and open-source web framework made by ',
_('a', {href: 'https://ah4.fr/'}, [
'©AH4',
_('sup', {className: 'bi-box-arrow-up-right'})
]),
'. It is a framework that allows you to create websites and web applications very quickly and easily. It is client-side rendered with Javascript which means that the website is loaded once and then only the content changes but still can manage several pages and be indexed by search engines. How ? It is based on a virtual DOM and a diffing algorithm that allows to update only the necessary parts of the DOM (with ',
_('b', 'Live variables'),
'). It also has a component system that allows you to create your own components and smartly reuse them in your pages.',
_('br'),
_('br'),
' To fully develop your website you will also need back-end code. Ophose has been designed to be used with its own secured back-end written in PHP and mainly working with ',
_('b', 'environments'),
'. Environments are a way to manage and create a whole system for your website. You can create as many environments as you want and each environment can communicate with each other. ',
_('br'),
'For example, let\'s say you have an Ophose web application for a forum. In order to manage the users, you will need a database. You can create an environment for the database and then create an environment for the users that will require the database environment. You can also create an environment for the forum itself that will require the users environment and so on. Ophose has been designed to be very modular, secure and easy to use. Making the bridge between the front-end and the back-end is very easy and can be done in a few lines of code. Please check the ',
new Link({href: '/tutorial', c: _('u', 'tutorial')}),
' for more information.'
]
});
}
};
class AboutWhy extends AboutSection {
constructor() {
super({
id: 'why',
title: 'Why should you move to Ophose ?',
description: [
'First of all, Ophose is ',
_('b', 'free and open-source'),
' meaning that you can use it for free and modify it as you want. It is also very lightweight and portable but powerful and limitless. If you already have developed a website or a web application based off vanilla PHP, you can easily migrate to Ophose to make your website more secure, more modular and more powerful. Ophose is also very easy to use and to learn. It implements native security features such as ',
_('b', 'CSRF protection'),
' and ',
_('b', 'XSS protection'),
'. It is constantly updated and maintained by AH4. It is also very easy to deploy and to host. You can deploy your website on an Apache server at the moment but we are working on a NGINX version. You can also deploy your website on a shared hosting or on a VPS. Ophose is also very fast as it uses very few resources and only vanilla Javascript. It is very easy to debug and to maintain without any external tools. With the ',
new Link({href: '/store', c: _('u', 'Ophose store')}),
', you can also easily get environment and components to use in your website making it even more powerful and faster to develop.',
_('br'),
_('br'),
'Ophose is also very flexible and can be used in many different ways. You can use it to create a simple website with a few pages or a complex web application with a lot of pages and features. Why ? Because',
_('ul', [
_('li', [_('b', 'Webapp build with Javascript components:'), ' you can create your own components and reuse them in your pages.']),
_('li', [_('b', 'Client-side rendered:'), ' the website is loaded once and then only the content changes but still can manage several pages and be indexed by search engines.']),
_('li', [_('b', 'Very fast:'), ' Ophose uses very few resources and only vanilla Javascript to render load. Plus, it is done asynchronously when possible.']),
_('li', [_('b', 'Environment system:'), ' you can create and use as many environments as you want. Each environment can communicate with each other and implement a safe-way to communicate with the front.']),
_('li', [_('b', 'API system:'), ' you can create and your own API endpoints to share data such as JSON, Images, etc...']),
_('li', [_('b', 'Secure:'), ' Ophose implements native security features such as CSRF protection and XSS protection.']),
_('li', [_('b', 'Easy to use:'), ' Ophose is very easy to use and to learn. It is also very easy to deploy and to host.'])
])
]
});
}
};
;
class AboutLicence extends AboutSection {
constructor(props) {
super({
id: 'licence',
title: 'What is the licence of Ophose ?',
description: [
'Ophose is an open-source project under the ',
new Link({href: '/licence', c: _('u', 'GPL licence with restrictions')}),
'. It means that you can use it for free and modify it as you want. However, you cannot sell or sub-sell this software. If you want to use Ophose for commercial purposes, please contact us at ',
_('a', {href: 'mailto:ah4ite@gmail.com'}),
'. Any violation of the licence will be legally prosecuted.'
]
});
}
};
class AboutFuture extends AboutSection {
constructor(props) {
super({
id: 'future',
title: 'What\'s next ?',
description: [
'Ophose is still in development, but it\'s already usable. You can already create your own components and pages, and use them in your projects. The development is still in progress, and many features are planned:',
_('ul', [
_('li', [_('b', 'Templates:'), ' create your project from a template which already contains some components, environments and pages. This will allow you to start your project faster.']),
_('li', [_('b', 'DevTools:'), ' a DevTools will be available to help you debug your project in real time.']),
]),
_('br'),
'If you have any idea or suggestion, feel free to leave an issue on the GitHub repository. You can also contribute to the project by creating a pull request. As Ophose is free and open source, donations are also welcome <3. You can also support the project by sharing it with your friends or colleagues. Feel free to donate at ',
_('a', {href: 'https://paypal.me/ah4x?country.x=FR&locale.x=fr_FR', children: [
_('u', ['here', _('sup', {className: 'bi bi-box-arrow-up-right'})])
]}),
'.'
]
});
}
}class AuthBanner extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
font-family: 'Roboto', sans-serif;
color: var(--text-color-light);
width: fit-content;
margin: 0 auto 1rem;
}
%self .banner_header {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
%self .banner_header img {
width: 32px;
height: 32px;
}
`
}
render() {
return {_: 'div', children: [
_('div', {className: 'banner_header'},
_('img', {src: '/img/ophose%20logo.png'}),
_('h4', 'OPHOSE AUTHENTICATION'),
)
]}
}
}class XForm extends Ophose.Component {
constructor(props) {
super(props);
if(this.props.dataEndpoint) {
oenv(this.props.dataEndpoint)
.then(r => {
this.updateFromData(r);
if(this.props.onDataLoaded) this.props.onDataLoaded(r);
});
}
}
updateFromData(data) {
let node = this.getNode();
for(let key in data) {
let input = node.querySelector(`*[name="${key}"]`);
if(!input) continue;
if(input.tagName.toLowerCase() === 'input' && input.getAttribute('type') === 'file') {
continue;
}
input.value = data[key];
input.dispatchEvent(new Event('input'));
}
}
style() {
return /* css */`
%self {
}
`
}
updateErrors(errors) {
let node = this.getNode();
node.querySelectorAll("*[name]").forEach((i) => {
let inputDiv = i.parentNode;
let name = i.getAttribute('name');
let divErrors = inputDiv.querySelector('.errors');
if(divErrors === null) return;
divErrors.innerHTML = '';
if(errors[name]) {
for(let error of errors[name]) {
let li = document.createElement('li');
li.textContent = et('error.' + error, {field: name});
divErrors.appendChild(li);
}
}
});
let firstError = node.querySelector('*[name]:has(.errors li)');
if(firstError) {
firstError.scrollIntoView({behavior: 'smooth'});
}
}
onSubmit(e) {
e.preventDefault();
let form = e.target;
let data = {};
let formData = new FormData(form);
let url = this.props.endpoint;
if(!url) return;
let submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
submitButton.classList.add('loading');
oenv(url, formData)
.then(r => {
if(this.props.onSuccess) this.props.onSuccess(r);
this.updateErrors([]);
submitButton.classList.remove('loading');
})
.catch(e => {
submitButton.classList.remove('loading');
let errors = e.responseJSON && e.responseJSON.errors || null;
if(errors) {
this.updateErrors(errors);
}
if(this.props.onError) this.props.onError(e);
});
}
render() {
return _('form', {onsubmit: this.onSubmit.bind(this)}, this.props.children);
}
}class XInput extends Ophose.Component {
constructor(props) {
super(props);
this.propsOn('input');
this.typesAsTag = ['select', 'textarea'];
if(!this.props.type) this.props.type = 'text';
if(!this.props.id) this.props.id = this.props.name;
if(!this.props.label) this.props.label = this.props.name.charAt(0).toUpperCase() + this.props.name.slice(1).replace('_', ' ');
if(!this.props.placeholder) this.props.placeholder = this.props.label;
this.tag = this.typesAsTag.includes(this.props.type) ? this.props.type : 'input';
this.value = new Live('');
this.inputChildren = undefined;
if(this.props.type == 'select' && this.props.options) {
this.inputChildren = this.props.options.map(option => {
return _('option', {value: option.value}, option.text)
});
}
}
style() {
return /* css */`
%self .char_counter {
text-align: right;
font-size: 0.75em;
}
`
}
onPlace(node) {
node.querySelector(this.tag).addEventListener('input', (e) => {
this.value.set(e.target.value);
});
}
render() {
return {_: 'div', className: 'form_group', children: [
_('label', {for: this.props.id}, this.props.label, this.props.required && _('sup', {style: 'color: red'}, '*')),
_(this.tag, {
_name: 'input',
children: this.inputChildren,
}),
this.props.maxlength && _('span', {className: 'char_counter'}, new PlacedLive(this.value, () => '' + this.value.value.length), '/' + this.props.maxlength),
_('ul', {className: 'errors'})
]}
}
}class ErosCaptcha extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
display: flex;
align-items: center;
padding: 2rem 0;
}
%self img {
style: width: 20%;
}
%self .refresh {
margin-left: 1rem;
cursor: pointer;
}
`
}
render() {
return _('div', {style: 'display: flex; align-items: center; padding: 2rem 0;'},
_('img', {src: '/@/eros/captcha/get', className: 'captcha_img'}),
_('a', {className: 'refresh bi bi-arrow-clockwise', onclick: () => {
this.getNode().querySelector('.captcha_img').src = '/@/eros/captcha/get';
}})
);
}
};
class AllDocs extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
display: flex;
flex-direction: column;
padding: 4rem 0;
gap: 1rem;
}
%self h1 {
font-size: 3rem;
}
%self > p {
font-size: 1.5rem;
font-weight: 300;
color: var(--text-color-light);
}
/* Docs list */
%self .docs-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
margin-top: 2rem;
}
%self .doc {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.5rem 0;
}
%self .doc h2 {
font-size: 1.5rem;
font-weight: 800;
letter-spacing: 0.05rem;
}
%self .doc a {
color: var(--text-color-light);
font-size: 1.2rem;
transition: color 0.2s;
}
%self .doc a:hover {
color: var(--text-color);
}
`
}
styles() {
return {
'md': /* css */`
%self .docs-list {
flex-direction: column;
text-align: center;
}
%self .doc {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.5rem 0;
}
`
}
}
render() {
return {_: 'div', className: 'wrapper', children: [
_('h1', {_: 'span', children: 'Documentation'}),
_('p', 'Welcome to the Ophose documentation! Here you will find all the information you need to fully understand Ophose and its concepts.'),
_('div', {className: 'docs-list', children: [
_('div', {className: 'doc', children: [
_('h2', {children: ['Frontend ', _('sup', {className: 'bi bi-filetype-js'})]}),
new Link({href: '/docs/front-components', children: 'Components'}),
new Link({href: '/docs/front-pages', children: 'Pages'}),
new Link({href: '/docs/front-base', children: 'Base'}),
new Link({href: '/docs/front-live', children: 'Live'}),
new Link({href: '/docs/front-modules', children: 'Modules'}),
new Link({href: '/docs/front-rendering', children: 'Rendering'}),
new Link({href: '/docs/front-importing', children: 'Importing'}),
new Link({href: '/docs/front-route', children: 'Route'}),
new Link({href: '/docs/front-environment', children: 'Environment'}),
new Link({href: '/docs/front-events', children: 'Events'}),
]}),
_('div', {className: 'doc', children: [
_('h2', {children: ['Backend ', _('sup', {className: 'bi bi-filetype-php'})]}),
new Link({href: '/docs/back-environment', children: 'Environment'}),
new Link({href: '/docs/back-command', children: 'Command'}),
new Link({href: '/docs/back-request', children: 'Request'}),
new Link({href: '/docs/back-response', children: 'Response'}),
new Link({href: '/docs/back-session', children: 'Session'}),
new Link({href: '/docs/back-configuration', children: 'Configuration files'}),
new Link({href: '/docs/back-cookie', children: 'Cookie'}),
new Link({href: '/docs/back-with', children: 'With'}),
new Link({href: '/docs/back-util', children: 'Util'}),
new Link({href: '/docs/back-app', children: 'App'}),
new Link({href: '/docs/back-template', children: 'Template'}),
]}),
_('div', {className: 'doc', children: [
_('h2', {children: ['Terminal ', _('sup', {className: 'bi bi-terminal'})]}),
new Link({href: '/docs/cmd-install', children: 'Install'}),
new Link({href: '/docs/cmd-build', children: 'Build'}),
new Link({href: '/docs/cmd-update', children: 'Update'}),
new Link({href: '/docs/cmd-trigger', children: 'Trigger'}),
]}),
_('div', {className: 'doc', children: [
_('h2', {children: ['Made with Ophose ', _('sup', {className: 'bi bi-app'})]}),
new Link({href: 'https://yzuvai.ah4.fr/', children: ['YzuvAI (AI music creation) ', _('sup', {className: 'bi bi-box-arrow-up-right'})]}),
new Link({href: 'https://ah4.fr/', children: ['AH4 (Web Portfolio) ', _('sup', {className: 'bi bi-box-arrow-up-right'})]}),
]}),
]})
]};
}
}class CodeBlock extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
border-radius: 0.5rem;
overflow: hidden;
font-size: 0.85em;
}
%self::-webkit-scrollbar {
width: 10px;
}
/* Track */
%self::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
%self::-webkit-scrollbar-thumb {
background: #888;
border-radius: 1rem;
}
/* Handle on hover */
%self::-webkit-scrollbar-thumb:hover {
background: #555;
}
`
}
onPlace(element) {
Prism.highlightAllUnder(element);
}
render() {
return _('pre', [
_('code', { className: `language-${this.props.lang ?? 'javascript'}` }, [
this.props.lines.join('\n')
])
])
}
};
;
class Documentation extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
/* Go back */
%self .go_back {
display: block;
color: var(--text-color-light);
margin: 1.5rem 0;
transition: color 0.2s;
font-weight: 600;
}
%self .go_back:hover {
color: var(--text-color);
}
/* Title */
%self h1 {
font-size: 3rem;
font-weight: 800;
}
/* Description */
%self .desc {
font-size: 1.5rem;
font-weight: 400;
color: var(--text-color-light);
margin: 1.5rem 0;
}
/* Content */
%self .t_content {
flex: 0.8;
max-width: 80%;
position: relative;
margin-bottom: 4rem;
}
%self .t_content li sup {
padding: 0.25rem 0.5rem;
}
%self .title {
font-size: 3rem;
font-weight: 800;
margin-bottom: 0.5rem;
}
%self .descripion {
font-size: 1.25rem;
font-weight: 400;
margin-bottom: 1rem;
}
%self h2 {
font-size: 1.75rem;
font-weight: 800;
margin: 6rem 0 2rem;
background-color: var(--comp-bg-color);
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
display: inline-block;
}
%self ul li :not(b) {
color: var(--text-color-light);
}
%self p {
font-size: 1.125rem;
font-weight: 500;
margin: 0.5rem 0;
font-family: var(--font-sen);
line-height: 2rem;
}
%self pre {
font-size: 0.9rem;
margin: 3rem 0;
}
%self h3 {
font-size: 1.5rem;
font-weight: 800;
margin: 3rem 0 1rem;
text-align: center;
}
%self a[href] {
color: var(--link-color);
transition: color 0.2s;
}
%self .t_content b {
background-color: var(--comp-bg-color);
padding: 0.25rem 0.5rem;
font-weight: 600;
border-radius: 0.5rem;
margin: 0.25rem 0;
display: inline-block;
}
%self .t_content img {
width: 100%;
border-radius: 0.5rem;
margin: 1rem 0;
}
%self .rendered {
margin: 1rem 0;
width: 100%;
border: var(--comp-border);
border-radius: 0.5rem;
overflow: hidden;
padding: 1rem;
transition: all 0.25s;
position: relative;
}
.rendered:hover {
padding: 1.125rem 1rem;
}
%self u {
display: block;
margin-top: 1rem;
}
%self h2 sup {
color: var(--text-color-light);
font-weight: 600;
font-size: 1rem;
padding: 0.25rem 0.5rem;
}
%self #toc li {
padding: 0.25rem 0;
}
%self #toc a {
color: var(--text-color-light);
transition: color 0.2s;
cursor: pointer;
user-select: none;
}
%self #toc a:hover {
color: var(--text-color);
}
`
}
styles() {
return {
'md': /* css */`
%self .t_content {
max-width: 100%;
}
`
}
}
onPlace(node) {
let toc = node.querySelector('#toc');
let headers = node.querySelectorAll('h2');
for (let header of headers) {
let id = header.innerText.replace(/ /g, '_');
header.id = id;
let name = "";
for (let child of header.childNodes) {
if (child.tagName == "SUP") continue;
name += child.nodeValue ?? child.innerText;
}
let li = document.createElement('li');
let a = document.createElement('a');
a.onclick = () => {
header.scrollIntoView({behavior: 'smooth'});
}
a.innerText = name;
li.appendChild(a);
toc.appendChild(li);
}
}
render() {
return {_: 'div', className: 'wrapper', children: [
new Link({href: '/docs', className: 'go_back', children: '< Back to docs'}),
_('h1', {_: 'span', children: this.title}),
_('p', {className: 'desc', children: this.description}),
_('br'),
_('br'),
_('h4', 'Table of contents:'),
_('ul', {className: 'toc', id: 'toc'}),
_('br'),
_('br'),
_('div', {className: 't_content'}, this.content)
]}
}
}
class DocFrontComponent extends Documentation {
constructor(props) {
super(props);
this.title = 'Components';
this.description = 'Ophose components are the building blocks of your application. They are the UI elements that you will use to build your application.';
this.content = [
_('h2', 'class Ophose.Component'),
_('p', ['All your components must extend the Ophose.Component class. This class provides you with all the necessary methods to create your components. To learn how to properly use it and create your components, you can read the tutorial at ', new Link({href: '/tutorials/components', children: 'this link'}), '.']),
_('hr'),
_('h2', 'Component(props = {})'),
_('p', [
'The constructor of your component. You can pass the props of your component as a parameter. You can access the props of your component using the this.props object.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'props'), ' (Object): The props of your component.'])
]),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Ophose.Component'), ': The component.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor(props) {",
" super(props);",
" }",
"}"
]})
]),
_('hr'),
_('h2', ['render()', _('sup', 'to be overridden')]),
_('p', [
'The render method of your component. This method will be called every time your component needs to be rendered. It must return a valid Ophose object.',
'A valid Ophose object is either a string, a number, an object with a _ property (its tag name) and optionally a children property with an array of valid Ophose objects (or a single valid Ophose object), an object extending the Ophose.Component class, a Live object, a PlacedLive object or a Node object.',
_('br'),
_('br'),
'By default, props passed to your component will be passed to the root node of your component meaning that for example, if you pass a className prop to your component, it will be applied to the root node of your component. And it will be the same for all the other props (id, style, etc...) and events (onclick, onmouseover, etc...).',
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Ophose object'), ': The element to render.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor(props) {",
" super(props);",
" }",
"",
" render() {",
" return {_: 'div', children: 'Hello World!'};",
" }",
"}"
]}),
'will render: ',
_('div', {className: 'rendered'}, [
'Hello ',
'World!'
])
]),
_('hr'),
_('h2', ['style()', _('sup', 'may be overridden')]),
_('p', [
'The style method of your component. This method will be called every time your component needs to be styled. It must return a valid CSS string.', _('br'),
'You can use the ', _('b', '%self'), ' selector target the current root node of your component.',
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'CSS string'), ': The CSS string to apply to your component.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor(props) {",
" super(props);",
" }",
"",
" style() {",
" return /* css */`",
" %self {",
" color: red;",
" }",
" `",
" }",
"",
" render() {",
" return {_: 'div', children: ['Hello ', 'World!']};",
" }",
"}"
]}),
_('p', 'will render: '),
_('div', {className: 'rendered', style: 'color: red'}, [
'Hello ',
'World!'
])
]),
_('hr'),
_('h2', ['styles()', _('sup', 'may be overridden')]),
_('p',
'The additional styles method of your component. This method will be called every time your component needs to be styled. It must return a valid object with the screen name as key and the CSS string as value. You may still use the ', _('b', '%self'), ' selector target the current root node of your component.',
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Object'), ': The additional styles as {screen: style} (for example: {md: `...`}) or null by default.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor(props) {",
" super(props);",
" }",
"",
" style() {",
" return /* css */`",
" %self {",
" color: blue;",
" }",
" `",
" }",
"",
" styles() {",
" return {",
" md: `",
" %self {",
" color: red;",
" }",
" `",
" }",
" }",
"",
" render() {",
" return {_: 'div', children: ['Hello ', 'World!']};",
" }",
"}"
]})
),
_('h2', ['onPlace(node)', _('sup', 'might be overridden')]),
_('p', [
'The onPlace method of your component. This method will be called every time your component is placed in the DOM. It can be useful if you need to do something with the root node of your component once it is placed in the DOM.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'node'), ' (Node): The root node of your component.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor(props) {",
" super(props);",
" }",
"",
" onPlace(node) {",
" console.log('My component has been placed in the DOM: ', node);",
" }",
"}"
]}),
' will log your component\'s root node in the console once it is placed in the DOM.'
]),
_('hr'),
_('h2', ['onRemove(node)', _('sup', 'might be overridden')]),
_('p', [
'The onRemove method of your component. This method will be called before your component is removed from the DOM. As onPlace(), it can be useful if you need to do something with the root node of your component once it is removed from the DOM.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'node'), ' (Node): The root node of your component.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor(props) {",
" super(props);",
" }",
"",
" onRemove(node) {",
" console.log('My component will be removed from the DOM: ', node);",
" }",
"}"
]}),
' will log your component\'s root node in the console once it is removed from the DOM.'
]),
_('h2', ['remove()']),
_('p', [
'The remove method of your component. This method will remove your component from the DOM and from Ophose instances. You should not override this method.'
]),
_('hr'),
_('h2', ['getNode()']),
_('p', [
'The getNode method of your component. This method will return the root node of your component. You should not override this method.',
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Node'), ': The root node of your component.'])
]),
]),
_('hr'),
_('h2', ['appendChild(child)']),
_('p', [
'The appendChild method of your component. This method will append a child to your component. You should not override this method.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'child'), ' (Ophose object): The child to append to your component.'])
]),
]),
_('hr'),
_('h2', ['addModule(', new Link({href: '/docs/front-modules', children: 'Ophose.Module'}), ')']),
_('p', [
'The addModule method will add a module to your component. You should not override this method.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'module'), ' (Ophose.Module): The module to add to your component.'])
]),
]),
_('h2', ['propsOn(childName)']),
_('p', [
'This method will pass the props of your component to the child in your component that has the property "_name" set to childName. You should not override this method.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'childName'), ' (String): The name of the child to pass the props to.'])
]),
]),
];
}
}
class DocFrontPages extends Documentation {
constructor(props) {
super(props);
this.title = 'Pages';
this.description = 'Ophose pages are the different pages of your application. They are the different views that your users will see depending on the URL they are on.';
this.content = [
_('h2', 'class Ophose.Page'),
_('p', ['All your pages must extend the Ophose.Page class. This class provides you with all the necessary methods to create your pages including their queries. To learn how to properly use it and create your pages, you can read the tutorial at ', new Link({href: '/tutorials/pages', children: 'this link'}), '. Note that the page must be shared with the ', _('b', 'oshare(PageClass)'), ' function. Also note that ', _('b', 'Ophose.Page'), ' extends ', _('b', 'Ophose.Component'), ' meaning that you can use all the methods of the ', new Link({href: '/docs/front-components', c: 'Ophose.Component'}), ' class.']),
_('hr'),
_('h2', 'Pages(urlQueries = {})'),
_('p', [
'The constructor of your page. The queries of your URL will be passed to ', _('b', 'urlQueries'), '.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'urlQueries'), ' (Object as {query: [queries], get: [get parameters]}): The queries of your page.'])
]),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Ophose.Page'), ': The page.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyPage extends Ophose.Page {",
" constructor(urlQueries) {",
" super();",
"",
" // Get the id query",
" this.userId = this.urlQueries.query.id;",
" }",
"}",
"",
"oshare(MyPage);"
]})
]),
_('hr'),
_('h2', 'page', _('sup', 'global')),
_('p', [
'This constant is the page that is currently loaded. It is a ', _('b', 'Ophose.Page'), ' object. You can access it from anywhere in your application once its loaded.',
]),
_('hr'),
_('h2', [new Link({href: '/docs/front-components#render()to_be_overridden', c: 'render()'}), _('sup', 'to be overridden')]),
_('p', [
'The render method of your page. This method will be called every time your page needs to be rendered. It must return a valid Ophose object. Click on the link above to learn more about the render method.',
]),
_('hr'),
_('h2', [new Link({href: '/docs/front-components#style()may_be_overridden', c: 'style()'}), _('sup', 'may be overridden')]),
_('p', [
'The style method of your page. This method will be called every time your page needs to be styled. It must return a valid CSS string. Click on the link above to learn more about the style method.',
]),
_('hr'),
_('h2', ['onLoad()', _('sup', 'might be overridden')]),
_('p', [
'This method will be called every time your page is loaded but yet not placed as it is already called in the ', _('b', 'onPlace'), ' method of Ophose.Component.',
]),
_('hr'),
_('h2', ['onCreate()', _('sup', 'might be overridden')]),
_('p', [
'This method will be called once your page is created. It is before after the ', _('b', 'onLoad'), ' method.',
_('br'),
_('br'),
'Note that if you want to redirect your page, you must call the ', _('b', 'redirect'), ' method of your page which should be placed in this event.',
]),
_('hr'),
_('h2', ['onLeave()', _('sup', 'might be overridden')]),
_('p', [
'This method will be called when your page is left.',
]),
_('hr'),
_('h2', ['redirect(url)']),
_('p', [
'This method will redirect your page to the given URL. It must be called in the ', _('b', 'onCreate'), ' method of your page.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'url'), ' (String): The URL to redirect to.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyPage extends Ophose.Page {",
" constructor(urlQueries) {",
" super();",
" }",
"",
" onCreate() {",
" if(this.urlQueries.query.action == 'go_home') this.redirect('/home');",
" }",
"}",
"",
"oshare(MyPage);"
]})
]),
];
}
}
class DocFrontBase extends Documentation {
constructor(props) {
super(props);
this.title = 'Base';
this.description = 'Everything displayed on your user interface will be a child of the Base component. It is the root component of your application which is useful to avoid your page reloading when you change the URL.';
this.content = [
_('h2', 'class Ophose.Base'),
_('p', ['Your root layout will be contained by the Base. Note that ', _('b', 'Ophose.Base'), ' extends ', _('b', 'Ophose.Component'), ' meaning that you can use all the methods of the ', new Link({href: '/docs/front-components', c: 'Ophose.Component'}), ' class. Also note that the Base must be named ', _('b', 'Base'), ' and must be in your components folder as ', _('b', 'Base.js'), '.']),
_('hr'),
_('h2', 'Base()'),
_('p', [
'The constructor of your root layout.', _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Ophose.Base'), ': The base.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class Base extends Ophose.Base {",
" constructor() {",
" super();",
" }",
"}"
]})
]),
_('hr'),
_('h2', [new Link({href: '/docs/front-components#render()to_be_overridden', c: 'render()'}), _('sup', 'to be overridden')]),
_('p', [
'The render method of your page. This method will be called once when the app is loaded. It must return a valid Ophose object. Click on the link above to learn more about the render method.',
]),
_('hr'),
_('h2', [new Link({href: '/docs/front-components#style()may_be_overridden', c: 'style()'}), _('sup', 'may be overridden')]),
_('p', [
'The style method of your base. This method will be called once your base is loaded. It must return a valid CSS string. Click on the link above to learn more about the style method.',
])
];
}
}
class DocFrontLive extends Documentation {
constructor(props) {
super(props);
this.counter = new Live(0);
this.counterPL = new Live(0);
this.title = 'Live';
this.description = 'Sometimes, you\'ll need to change the content of your page without reloading it. This is where the Live component comes in. It allows you to change the content of your page without reloading it from simple text to complex components.';
this.content = [
_('h2', 'class Live'),
_('p', ['A live is an object that can be rendered on your page. As said above, it can be a simple text or a complex component. It only contains a data to be displayed and useful methods to change and listen to it.']),
_('hr'),
_('h2', 'Live(defaultValue)'),
_('p', [
'The constructor of your Live.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'defaultValue'), ' (any): The value of your live.'])
]),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Live'), ': The live.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class Base extends Ophose.Base {",
" constructor() {",
" super();",
"",
" this.live = new Live('Hello world!');",
" }",
"}"
]})
]),
_('hr'),
_('h2', 'How to use it ?'),
_('p', [
'The live is an object that can be rendered on your page. As said above, it can be a simple text or a complex component. It only contains a data to be displayed and useful methods to change and listen to it. Let\'s admit you want to create a simple counter that will be displayed on your page. You can do it like this:',
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" // Create the counter Live with a default value of 0",
" this.counter = new Live(0);",
" }",
"",
" render() {",
" return {_: 'div', children: [",
" // Display the counter value (automatically updated)",
" _('p', {children: ['The counter value is: ', this.counter]}),",
"",
" // Increment the counter by 1 when the button is clicked",
" _('button', {onclick: () => {",
" this.counter.add(1);",
" }, children: 'Increment'}),",
" ]}",
" }",
"}"
]}),
'resulting in:',
_('div', {className: 'rendered', children: [
_('p', {children: ['The counter value is: ', this.counter]}),
_('button', {className: 'button', onclick: () => {
this.counter.add(1);
}, children: 'Increment'}),
]}),
]),
_('hr'),
_('h2', 'How to render complex Ophose objects with PlacedLive ?'),
_('p', [
'The PlacedLive is a special class that allows you to render Ophose objects with a Live variable is updated. Let\'s admit you want to render a component when the button is clicked. This should be used only in a Ophose.component#render method. Note that if you need to use multiple lives for a render, you can pass as many you want in parameters and your callback will be called according to these values. You can do it like this:',
_('u', 'Class: '),
_('b', 'PlacedLive(lives..., callbackForOphoseObject)'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'lives'), ' (Live[]): The livez to watch.']),
_('li', [_('b', 'callbackForOphoseObject'), ' (function): The callback that returns the Ophose object to render.'])
]),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'PlacedLive'), ': The placed live.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" // Create the counter Live with a default value of 0",
" this.counter = new Live(0);",
" }",
"",
" render() {",
" return {_: 'div', children: [",
" // Display the counter value (automatically updated)",
" _('p', {children: ['The counter value is: ', this.counter]}),",
"",
" // Increment the counter by 1 when the button is clicked",
" _('button', {onclick: () => {",
" this.counter.add(1);",
" }, children: 'Increment'}),",
"",
" // Render the component when the counter is >= 10",
" new PlacedLive(this.counter, () => {",
" if(this.counter.get() >= 10) {",
" return {_: 'div', children: 'The counter is greater than or equal to 10!'};",
" }",
" })",
" ]}",
" }",
"}"
]}),
'resulting in:',
_('div', {className: 'rendered', children: [
_('p', {children: ['The counter value is: ', this.counterPL]}),
_('button', {className: 'button', onclick: () => {
this.counterPL.add(1);
}, children: 'Increment'}),
new PlacedLive(this.counterPL, () => {
if(this.counterPL.get() >= 10) {
return _('div', {children: 'The counter is greater than or equal to 10!'});
}
return _('div');
})
]}),
'You may want to see the ', new Link({href: '/tutorials/components', c: 'tutorial about Components & Live'}), ' to learn more about this.',
]),
_('hr'),
_('h2', 'get()'),
_('p', [
'Returns live value and automatically assigns as observer of this (if placed to be automatically updated). Note that in most cases, you won\'t need to use this method to get the value of your live as you can directly get it by using the live as a string (see the example above).',
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'any'), ': The value.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" // Create the counter Live with a default value of 0",
" this.counter = new Live(0);",
"",
" // Get the counter value",
" let counterValue = this.counter.get();",
" }",
"}"
]})
]),
_('hr'),
_('h2', 'set(value)'),
_('p', [
'Updates the value.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'value'), ' (any): The value.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" // Create the counter Live with a default value of 0",
" this.counter = new Live(0);",
"",
" // Set the counter value to 10",
" this.counter.set(10);",
" }",
"}"
]})
]),
_('hr'),
_('h2', 'add(value)'),
_('p', [
'Adds a value to the current value.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'value'), ' (any): The value to add.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" // Create the counter Live with a default value of 0",
" this.counter = new Live(10);",
"",
" // Add 10 to the counter",
" this.counter.add(10);",
" // The counter value is now 20",
" }",
"}"
]})
]),
_('hr'),
_('h2', 'remove(value)'),
_('p', [
'Removes a value from the current value.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'value'), ' (any): The value to remove.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" // Create the counter Live with a default value of 0",
" this.counter = new Live(10);",
"",
" // Remove 10 from the counter",
" this.counter.remove(10);",
" // The counter value is now 0",
" }",
"}"
]})
]),
_('hr'),
_('h2', 'toggle()'),
_('p', [
'Toggles the value between true and false. Note that this method only works if the value is a boolean.',
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'boolean'), ': The new value.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" this.checked = new Live(true);",
"",
" // Toggle the checked value",
" this.checked.toggle();",
" // The checked value is now false",
" }",
"}"
]})
]),
_('hr'),
_('h2', 'addCallbackListener(callback)'),
_('p', [
'Adds a callback listener.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'callback'), ' (function): The callback.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" // Create the counter Live with a default value of 0",
" this.counter = new Live(10);",
"",
" // Add a callback listener",
" this.counter.addCallbackListener(() => {",
" console.log('The counter value has changed!');",
" });",
" }",
"}"
]})
]),
_('hr'),
_('h2', 'removeCallbackListener(callback)'),
_('p', [
'Removes a callback listener.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'callback'), ' (function): The callback.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" // Create the counter Live with a default value of 0",
" this.counter = new Live(10);",
"",
" // Add a callback listener",
" this.callback = () => {",
" console.log('The counter value has changed!');",
" };",
" this.counter.addCallbackListener(this.callback);",
"",
" // Remove the callback listener",
" this.counter.removeCallbackListener(this.callback);",
" }",
"}"
]})
]),
_('hr'),
_('h2', 'Live.local(key, defaultValue)', _('sup', 'static')),
_('p', [
'Creates or get a local live. A local live is a live that is saved in the local storage of the user. It is useful to save data that must be kept even if the user reloads the page.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'key'), ' (string): The key of the local live.']),
_('li', [_('b', 'defaultValue'), ' (any): The default value of the live.'])
]),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Live'), ': The live.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"class MyComponent extends Ophose.Component {",
" constructor() {",
" super();",
"",
" // Create o get the counter Live with a default value of 0 if not set",
" this.counter = Live.local('counter', 0);",
" }",
"}"
]})
]),
];
}
}
class DocFrontModules extends Documentation {
constructor(props) {
super(props);
this.title = 'Modules';
this.description = 'By developing your application with Ophose, you may create repetitive components. To avoid this, you can create modules which are designed to handle events and data. For example, you can create a module to handle a form and then use it in multiple components.';
this.content = [
_('h2', 'class Ophose.Module'),
_('p', ['Your root layout will be contained by the Base. Note that ', _('b', 'Ophose.Base'), ' extends ', _('b', 'Ophose.Component'), ' meaning that you can use all the methods of the ', new Link({href: '/docs/front-components', c: 'Ophose.Component'}), ' class. Also note that the Base must be named ', _('b', 'Base'), ' and must be in your components folder as ', _('b', 'Base.js'), '.']),
_('hr'),
_('h2', 'Module()'),
_('p', [
'The constructor of your module.'
]),
_('hr'),
_('h2', ['onComponentAdded(' , new Link({href: '/docs/front-components', c: 'Component'}) ,')']),
_('p', [
'This method will be called every time your module is added to a component.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'component'), ' (', new Link({href: '/docs/front-components', c: 'Component'}), '): The component that added the module.'])
]),
]),
_('hr'),
_('h2', ['onComponentRemoved(' , new Link({href: '/docs/front-components', c: 'Component'}) ,')']),
_('p', [
'This method will be called every time your module is removed from a component.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'component'), ' (', new Link({href: '/docs/front-components', c: 'Component'}), '): The component that removed the module.'])
]),
]),
_('hr'),
_('h2', 'getComponents()'),
_('p', [
'This method will return all the components that added the module.', _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Array'), ': The components that added the module.'])
]),
]),
_('hr'),
_('h2', ['removeComponent(', new Link({href: '/docs/front-components', c: 'Component'}), ')']),
_('p', [
'This method will remove a component from the module.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'component'), ' (', new Link({href: '/docs/front-components', c: 'Component'}), '): The component to remove.'])
]),
]),
_('hr'),
_('h2', ['onPlace(', new Link({href: '/docs/front-components', c: 'Component'}), ', HTMLElement)']),
_('p', [
'This method will be called every time your module is placed in a component.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'component'), ' (', new Link({href: '/docs/front-components', c: 'Component'}), '): The component that placed the module.']),
_('li', [_('b', 'element'), ' (HTMLElement): The DOM element that contains the module.'])
]),
]),
_('hr'),
_('h2', ['onRemove(', new Link({href: '/docs/front-components', c: 'Component'}), ', HTMLElement)']),
_('p', [
'This method will be called every time your module is removed from a component.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'component'), ' (', new Link({href: '/docs/front-components', c: 'Component'}), '): The component that removed the module.']),
_('li', [_('b', 'element'), ' (HTMLElement): The DOM element that contained the module.'])
]),
]),
];
}
}
class DocFrontRendering extends Documentation {
constructor(props) {
super(props);
this.title = 'Rendering';
this.description = 'Rendering is the process of displaying your components on the screen. There are several concepts to understand to properly render your components.';
this.content = [
_('h2', 'What is an Ophose object?'),
_('p', [
'As said before, when you call the ', _('b', 'Ophose.Component#render'), ' method, it must return a single valid Ophose object. An Ophose object is an object that describes how to render your component. To be valid, it must be as:',
_('ul', [
_('li', [_('b', 'string'), ': A string will be rendered as a text node.']),
_('li', [_('b', 'number'), ': A number will also be rendered as a text node.']),
_('li', [_('b', 'Ophose.Component'), ': A component render will be transformed into an HTML element but will still have all the methods of the ', new Link({href: '/docs/front-components', c: 'Ophose.Component'}), ' class and its properties.']),
_('li', [_('b', 'Live'), ': A Live object will be rendered as a text node but will be updated every time the value of the Live object changes.']),
_('li', [_('b', 'PlacedLive'), ': A PlacedLive object will be rendered as an Ophose object but will be updated and replaced every time the value of the PlacedLive object changes.']),
_('li', [_('b', 'Node'), ': A Node object will be rendered as node but.']),
_('li', [_('b', 'object'), ': An object describing your Ophose object... It must have an "_" key which is the tag of your HTML element. It can also have a "c" or "children" key which is the children of your HTML element.']),
_('li', [_('b', _('i', 'Array[Ophose Object]')), ': An array will be transformed into a fragment of HTML elements. Note that it will be flattened.']),
]),
]),
_('hr'),
_('h2', 'About pure Ophose objects'),
_('p', [
'You may have noticed that the Ophose object can be an object with an "_" key. This is a pure Ophose object. It is an object that describes how to render your component. It is pure because it does not have any methods or properties. It is just a plain object. It is useful to create your own components and to pass them to the ', _('b', 'Ophose.Component#render'), ' method. For example, you can create a component that will render a title and then use it in your other components.',
_('br'),
_('br'),
'Only the "_" key is mandatory. It can have children of Ophose objects in the "c" or "children" key. It can also have a "text" key which will be rendered as a text node. To add a class to your HTML element, you can use the "className" key. Every other pure HTML attributes can be added to your object. For example, you can add an "id" key to your object to add an id to your HTML element. You can also add an event listener to your HTML element by adding a key starting with "on" followed by the event name. For example, you can add an "onclick" key to your object to add an onclick event listener to your HTML element. Note that the value of the event listener must be a function. Also, if a value is ', _('b', 'undefined'), ' it will not be added to the HTML element.',
]),
_('h2', '_(tag, ...propsOrChildren?)'),
_('p', [
'This function is a shortcut to create an Ophose object.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'tag'), ' (string): The HTML tag.']),
_('li', [_('b', 'propsOrChildren = {}'), ' (object|string|number|Array|Ophose.Component|Live|PlacedLive|Node): The props if object without "_" key, else children. Note that they can be additioned and if they are, they will be flattened.']),
]),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'object'), ': The Ophose object.'])
]),
_('u', 'Examples: '),
new CodeBlock({lines: [
'_("h1", "I am a title")',
'// Will be rendered as
I am a title
',
'',
'_("div", {className: "my-class"}, "I am a div")',
'// Will be rendered as I am a div
',
'',
'_("div", {className: "my-class"}, _("h1", "I am a title"))',
'// Will be rendered as ',
'//
I am a title
',
'// ',
'',
'_("ul", {id: "ul"}, [',
' _("li", "Item 1"),',
' _("li", "Item 2"),',
' _("li", "Item 3"),',
'])',
'// Will be rendered as ',
'// - Item 1
',
'// - Item 2
',
'// - Item 3
',
'//
',
]})
]),
_('hr'),
_('h2', 'Conditional rendering'),
_('p', [
_('h3', 'Conditional rendering with the && operator (static)'),
'You can use the && operator to render a component only if a condition is true. For example, you can render a component only if a variable is true. But the render will be static and will not be updated if the variable changes.',
_('br'),
_('br'),
'Example:',
new CodeBlock({lines: [
'class MyComponent extends Ophose.Component {',
' constructor() {',
' super();',
'',
' this.show = true;',
' }',
'',
' render() {',
' return _("div", [',
' this.show && _("h1", "I am a title")',
' ]);',
' }',
'}',
]}),
_('h3', 'Conditional rendering with PlacedLive (dynamic)'),
'You may want to create a Live object containing a boolean and then render a component only if the value of the Live object is true. To do so, you can use the PlacedLive object to render as seen in the ', new Link({href: '/docs/front-live#How_to_render_complex_Ophose_objects_with_PlacedLive_?', c: 'PlacedLive'}), ' documentation. Also note that you must return a single Ophose object in the PlacedLive callback.',
_('br'),
_('br'),
'Example:',
new CodeBlock({lines: [
'class MyComponent extends Ophose.Component {',
' constructor() {',
' super();',
'',
' this.show = new Live(true);',
' }',
'',
' render() {',
' return _("div", [',
' new PlacedLive(this.show, (show) => {',
' return _("h1", show && "I am a title");',
' })',
' ]);',
' }',
'}'
]}),
]),
];
}
}
class DocFrontRoute extends Documentation {
constructor(props) {
super(props);
this.title = 'Route';
this.description = 'Routes are used to display a page when the URL changes. They are the equivalent of the routes in a classic web application.';
this.content = [
_('h2', 'class route'),
_('p', ['The route class only contains the static method ', _('b', 'go'), ' which is used to change the URL without reloading the page.']),
_('hr'),
_('h2', ['route.go(url) ', _('sup', 'static')]),
_('p', [
'This method is used to change the URL without reloading the page. If the URL is a valid URL, the page will be redirected to the URL. Otherwise, the page will be redirected to the page corresponding to the URL. ', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'url'), ' (String): The URL to go to.'])
]),
_('u', 'Examples: '),
new CodeBlock({lines: [
"route.go('/my-page'); // Go to the page /my-page of your application",
"route.go('https://google.com'); // Go to google.com"
]})
]),
];
}
}
class DocFrontImporting extends Documentation {
constructor(props) {
super(props);
this.title = 'Importing';
this.description = 'In order to use Ophose, you must import your components and modules as they are in different files.';
this.content = [
_('h2', 'How does import work?'),
_('p', ['When you import a component or a module, you are actually running the code of the file you are importing. This means that if you writing code outside of a class or a function, it will be executed. But what would happen if you declare a class or a function twice? It\'d throw an error. That\'s why you can only import a component or a module once but Ophose will automatically import your components and modules in the right order and once. Meaning that even if you import a component or a module multiple times, it will only be imported once. By convention, you should name your components and modules with a capital letter and as same as your file. Also note that your import statements must be at the top of your file.']),
_('hr'),
_('h2', 'oimpc(path)'),
_('p', ['This is the syntax to import a component. The path is relative to the ', _('b', '/components/'), ' folder without the .JS extension. Meaning that if you want to import a component that is in ', _('b', '/components/layout'), ' and is named ', _('b', 'List.js'), ', you\'ll have to write ', _('b', 'oimpc("layout/List")'), '.']),
_('h2', 'oimpm(path)'),
_('p', ['This is the syntax to import a module. The path is relative to the ', _('b', '/modules/'), ' folder without the .JS extension. Meaning that if you want to import a module that is in ', _('b', '/modules'), ' and is named ', _('b', 'Form.js'), ', you\'ll have to write ', _('b', 'oimpm("Form")'), '.']),
_('h2', 'oimpe(path)'),
_('p', ['This is the syntax to import an environment component. The path is relative to the ', _('b', '/env/YourEnvironment'), ' folder without the .JS extension. Meaning that if you want to import an environment that is in ', _('b', '/env/MyEnvironment/'), , ' or ', _('b', '/env/.ext/Author/TheirEnvironment'), ' and is named ', _('b', 'Form.js'), ', you\'ll have to write ', _('b', 'oimpe("MyEnvironment/Form")'), '.']), _('br'), _('p', 'Note if you have a env.js file at ', _('b', '/env/MyEnvironment/env.js'), ', you can import it by writing ', _('b', 'oimpe("MyEnvironment")'), '.')
];
}
}
class DocFrontEnvironment extends Documentation {
constructor(props) {
super(props);
this.title = 'Environment';
this.description = 'You now need to communicate with your server. To do so, you\'ll have to talk with your environments. Environments may be designed to handle your requests via endpoints. Please make sure to understand important concepts such as endpoints, requests and responses through tutorials.';
this.content = [
_('h2', ['oenv(endpoint, data)', _('sup', 'async')]),
_('p', [
'This is the syntax to send a POST request to an Ophose environment. The endpoint may have been declared by the environment itself. The data is the data you want to send to the endpoint. The data can be anything and will be automatically converted to be sent. Also, if the key "options" is present in the data, it will be used as the options for the request. The options are:',
_('ul', [
_('li', [_('b', 'method'), ' (', _('b', 'String'), '): The method of the request.']),
_('li', [_('b', 'headers'), ' (', _('b', 'Object(headerKey: value)'), '): The headers of the request.']),
_('li', [_('b', 'toFormData'), ' (', _('b', 'Boolean'), '): If the data should be sent as FormData (false by default).']),
]),
_('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'endpoint'), ' (', _('b', 'String'), '): The full path of the endpoint. (for example: ', _('code', '/myEnv/myEndpoint'), ')']),
_('li', [_('b', 'data'), ' (', _('b', 'Object'), '): The data to send.']),
]),
_('u', 'Returns (asynchronously): '),
_('ul', [
_('li', [_('b', 'Object'), ': The response of the request.']),
_('li', [_('b', 'Object'), ': If the request failed.'])
]),
_('u', 'Example: '),
new CodeBlock({lines: [
"let response = await oenv('myEnv/myEndpoint', {",
" name: 'John',",
" age: 18",
"});"
]})
]),
];
}
}
class DocFrontEvents extends Documentation {
constructor(props) {
super(props);
this.title = 'Events';
this.description = 'Ophose provides a way to handle events. You can listen to events and trigger them.';
this.content = [
_('h2', 'class Ophose.Event'),
_('p', ['This class contains all the static methods to handle events.']),
_('hr'),
_('h2', 'Ophose.Event.addListener(eventName, callback) ', _('sup', 'static')),
_('p', [
'This method will add a listener to an event.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'eventName'), ' (', _('b', 'String'), '): The name of the event.']),
_('li', [_('b', 'callback'), ' (', _('b', 'Function'), '): The callback to call when the event is triggered.']),
]),
]),
_('hr'),
_('h2', 'Ophose.Event.callEvent(eventName, value)'),
_('p', [
'This method will trigger an event.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'eventName'), ' (', _('b', 'String'), '): The name of the event.']),
_('li', [_('b', 'value'), ' (', _('b', 'Object'), '): The value to send to the callback.']),
]),
]),
_('hr'),
_('h2', 'All Ophose events'),
_('h3', 'onPageLoad'),
_('p', [
'This event is triggered when the page is about to be loaded.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'url'), ' (', _('b', 'String'), '): URL of the page.']),
]),
]),
_('h3', 'onPageLoaded'),
_('p', [
'This event is triggered when the page is loaded.',
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'url'), ' (', _('b', 'String'), '): URL of the page.']),
]),
]),
];
}
}
class DocBackEnvironment extends Documentation {
constructor(props) {
super(props);
this.title = 'Environment';
this.description = 'An environment can be seen as brick of your application backend. It is a class that will handle a specific part of your application. For example, you can create an environment to handle your database, another one to handle your users, etc.';
this.content = [
_('h2', 'How to properly create an environment?'),
_('p', [
'As said above, an environment is a specific part of your application. It doesn\'t have to have endpoints nor commands for example. However, it may still useful... For example, you wouldn\'t want to create a database environment with endpoints for security reasons. But you may want to create a user environment with endpoints to handle the user authentication that may require your database environment.',
_('br'),
_('br'),
'To create a valid environment, you first have to create a folder named after your environment (for example: Database, User, Product...) in your ', _('b', '/env'), 'folder then you must eiher create a file ', _('b', 'env.php'), ' returning your environment class (we will cover it later) or a file ', _('b', 'env.oconf'), ' containing your environment configuration. Note that you can create both files for sure, it just allows Ophose to find your environment.',
_('br'),
_('br'),
]),
_('hr'),
_('h2', 'Constants'),
_('p', [
'Ophose provides some constants to help you handle the environment. Here is the list of the constants: ',
_('ul', [
_('li', [_('b', 'ROOT'), ': The root of your application.']),
_('li', [_('b', 'ENV_PATH'), ': The path of your environments (/env/ folder).']),
_('li', [_('b', 'CONFIG'), ': The project configuration as associative array.']),
]),
]),
_('hr'),
_('h2', 'class Ophose\Env'),
_('p', [
'The environment you return must extend this class. This class will be used by Ophose to handle your environment.',
]),
_('hr'),
_('h2', 'Env::environmentExists($path)'),
_('p', [
'This method will return whether the environment exists or not.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'path'), ' (', _('b', 'String'), '): The path to the environment.'])
]),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'boolean'), ': Whether the environment exists or not.'])
]),
]),
_('hr'),
_('h2', 'Env::getEnvironment($path)'),
_('p', [
'This method will return the environment instance.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'path'), ' (', _('b', 'String'), '): The path to the environment.'])
]),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Ophose\Env|false'), ': The environment or false if not found.'])
]),
]),
_('hr'),
_('h2', ['Env#endpoints()', _('sup', 'may be overridden')]),
_('p', [
'This method will return the endpoints of your environment. This method is interesting if you want to create an environment with endpoints and conditionally add them. For example, you may want to add endpoints only if the user is authenticated.', _('br'),
'To create one endpoint, you must call the ', _('b', 'endpoint($url, $callback, $csrf, $methods, $required)'), ' method of your environment (which is explained in the next point).'
]),
_('hr'),
_('h2', ['Env#endpoint($url, $callback, $csrf, $methods, $required)']),
_('p', [
'This method will add an endpoint to your environment when required.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'url'), ' (', _('b', 'String'), '): The URL of the endpoint.']),
_('li', [_('b', 'callback'), ' (', _('b', 'String'), '): The callback of the endpoint.']),
_('li', [_('b', 'csrf = false'), ' (', _('b', 'boolean'), '): Whether the endpoint requires a CSRF token or not (it is automatically transfered by Ophose with oenv method).']),
_('li', [_('b', 'methods = []'), ' (', _('b', 'Array'), '): The methods accepted by the endpoint. (all if empty)']),
_('li', [_('b', 'required = []'), ' (', _('b', 'Array'), '): Fields ($_POST or $_GET depending the methods) required by the endpoint.']),
]), _('br'),
'Note that the callback must can be a function or a method of your environment.',
_('br'), _('br'),
'URL can contain parameters. For example, if you want to create an endpoint to get a user by its ID, you can write ', _('b', '/user/_id'), ' and then you can get the ID in your function callback arguments as ', _('b', '$callback($id)'), '. The parameters are added by the order they are in the URL.',
_('br'), _('br'),
'Then the endpoint can be called either with the ', new Link({href: '/docs/front-environment', c: 'oenv()'}), ' method or by calling the URL directly at the root of your application preceded by ', _('b', '/@/'), '. For example, if you have an environment named "Profile" and you created an endpoint ', _('b', '/user/_id'), ', you can call it with ', _('b', '/@/profile/user/1234'), '.',
]),
_('hr'),
_('h2', ['Env#commands()', _('sup', 'may be overridden')]),
_('p', [
'This method will return the commands of your environment.', _('br'),
'To create one command, you must call the ', _('b', 'command($name, $callback, $description = null)'), ' method of your environment (which is explained in the next point).'
]),
_('hr'),
_('h2', ['Env#command($name, $callback, $description = null)']),
_('p', [
'This method will add a command to your environment when required.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'name'), ' (', _('b', 'String'), '): The name of the command.']),
_('li', [_('b', 'callback'), ' (', _('b', 'String'), '): The callback of the command.']),
_('li', [_('b', 'description = null'), ' (', _('b', 'String'), '): The description of the command.']),
]), _('br'),
'Note that the callback must can be a function or a method of your environment.',
_('br'), _('br'),
'Then the command will be called via the command line with the following syntax: ', _('b', 'php ocl [args...]'), '. For example, if you have an environment named "Profile" and you created a command ', _('b', 'user'), ', you can call it with ', _('b', 'php ocl profile user'), '.',
_('br'), _('br'),
'The callback will receive the current ', new Link({href: '/docs/back-command', c: 'Ophose\\Command'}), ' instance as argument which contains the arguments, options and useful methods to handle your command.',
]),
_('hr'),
_('h2', ['Env#onInstall()', _('sup', 'may be overridden')]),
_('p', [
'This method will be called when the environment is fetched to be installed. This is useful to create tables, add data, etc.',
]),
_('h2', ['Environment configuration']),
_('p', [
'Your environment configuration must be written in a file named ', _('b', 'env.oconf'), ' in your environment folder. This file must return an object with the following properties:', _('br'),
_('u', 'Properties: '),
_('ul', [
_('li', [_('b', 'name'), ' (', _('b', 'String'), '): The name of your environment.']),
_('li', [_('b', 'description'), ' (', _('b', 'String'), '): The description of your environment.']),
_('li', [_('b', 'version'), ' (', _('b', 'String'), '): The version of your environment.']),
_('li', [_('b', 'author'), ' (', _('b', 'String'), '): The author of your environment.']),
_('li', [_('b', 'dependencies'), ' (', _('b', 'Array'), '): The dependencies of your environment.']),
_('li', [_('b', 'devDependencies'), ' (', _('b', 'Array'), '): The dev dependencies of your environment.']),
_('li', [_('b', 'autoload'), ' (', _('b', 'Array'), '): The folders containing the classes of your environment you\'d like to be able to autoload. (note that filenames must be as the same as the class).']),
]), _('br'),
'Note that the dependencies and devDependencies must be written as ', _('b', 'name: version'), '. For example, if you want to add the AH4/Database dependency, you can add the entry ', _('b', '"AH4/Database": "1.0.0"'), '.',
_('br'), _('br')
]),
_('hr'),
];
}
}
class DocBackCommand extends Documentation {
constructor(props) {
super(props);
this.title = 'Command';
this.description = 'Commands stands for the command input on the terminal. They are useful to create a CLI (Command Line Interface) for your application and making only-adminstrator processes.';
this.content = [
_('h2', 'class \\Ophose\\Command'),
_('p', ['This class contains all the methods you need to interact and get data from your command such as the arguments, the options... Note that this class refers to the command called on the terminal and not an actual command process. To run a command, simply type "php ocl [arguments] [--options]". For example, if you have an environment called "myenv" and a command called "mycommand", you can run the command by typing "php ocl myenv mycommand" or if it is from an installed environment, you can run it by typing "php ocl author:environment [arguments]".']),
_('hr'),
_('h2', 'Command#getArguments()'),
_('p', [
'This method will return the arguments of your command.', _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Array'), ': The arguments of your command.'])
]),
]),
_('hr'),
_('h2', 'Command#getCommandName()'),
_('p', [
'This method will return the name of your command.', _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'String'), ': The name of your command.'])
]),
]),
_('hr'),
_('h2', 'Command#hasOption($optionName)'),
_('p', [
'This method will return whether the option is set or not. An option is an argument preceded by "-" or "--".', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'optionName'), ' (', _('b', 'String'), '): The name of the option.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'boolean'), ': Whether the option is set or not.'])
]),
]),
_('hr'),
_('h2', 'Command#getOption($optionName)'),
_('p', [
'This method will return the value of the option. An option is an argument preceded by "-" or "--".', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'optionName'), ' (', _('b', 'String'), '): The name of the option.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'String|null|false'), ': The value of the option or null if the option is set but has no value or false if the option is not set.'])
]),
]),
_('hr'),
_('h2', 'Command#hasArgument($argumentName, $argsOffset = 3)'),
_('p', [
'This method will return whether the argument is set or not.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'argumentName'), ' (', _('b', 'String'), '): The name of the argument.']),
_('li', [_('b', 'argsOffset = 3'), ' (', _('b', 'int'), '): The offset of the arguments. By default, it is 3 because the first argument is the command name, the second is the environment and the third is the command name.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'boolean'), ': Whether the argument is set or not.'])
]),
]),
_('hr'),
_('h2', 'Command#getOptions()'),
_('p', [
'This method will return the options of your command.', _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Array'), ': The options of your command.'])
]),
]),
];
}
}
class DocBackRequest extends Documentation {
constructor(props) {
super(props);
this.title = 'Request';
this.description = 'The request is what the client sends to the server. It contains the URL, the method, the headers, the body... Let\'s see how Ophose handle them.';
this.content = [
_('h2', 'What happens when a request is sent?'),
_('p', ['By default, when a HTTP request in sent to the server, Ophose will handle it. There are several steps that Ophose will follow to handle to return the expected response. In Order, Ophose will check then return:']),
_('ol', [
_('li', 'Disable access to files and folders that are not supposed to be accessed and return an error if the client tries to access them.'),
_('li', [_('b', 'If URL matches: /@/*, /@env/*, /api/*, /@api/*'), ' Check if the request is calling an environment endpoint and then return the response of the endpoint if valid or an error if not.']),
_('li', [_('b', 'If URL matches: /@dep/*'), ' Check if the request is calling a dependency and then return the file if any (jQuery for example...).']),
_('li', [_('b', 'If URL matches: /@ojs/*'), ' Check if it\'s calling a JS Ophose file.']),
_('li', [_('b', 'If URL matches: /@component/* or /@module/*'), ' Check if it\'s calling a component or module.']),
_('li', [_('b', 'If URL matches: /@query/*'), ' Check if it is calling a page query.']),
_('li', [_('b', 'If URL matches: /@pages/* or /pages/*'), ' Check if it is calling a page.']),
_('li', [_('b', 'If URL matches: /@public/* or /public/* or /*'), ' Check if it is calling a file in your /public folder.']),
_('li', [_('b', 'Finally'), ' It will go to your Ophose application and process the request as a normal request.']),
]),
_('hr'),
_('h2', 'Constants'),
_('p', [
'Ophose provides some constants to help you handle the request. Here is the list of the constants: ',
_('ul', [
_('li', [_('b', 'FULL_REQUEST_HTTP_URL'), ': The full URL of the request. This is the URL that the client sent to the server. For example: /my-page?param1=value1¶m2=value2']),
_('li', [_('b', 'REQUEST_HTTP_URL'), ': The URL of the request. This is the URL that the client sent to the server without the query parameters. For example: /my-page']),
_('li', [_('b', 'REQUEST_METHOD'), ': The method of the request. This is the method that the client sent to the server. For example: GET']),
]),
]),
_('hr'),
_('h2', '\\Ophose\\Request'),
_('p', [
'This class contains all the methods you need to interact and get data from your request such as the URL, the method, the headers, the body... Note that this class refers to the request sent by the client and not an actual request process. Note that this class only contains static methods as a request is only sent once and is not supposed to be modified',
]),
_('hr'),
_('h2', 'Request::query($key)'),
_('p', [
'This method will return the query parameter of the request. This will look through the $_GET variable.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'key'), ' (', _('b', 'String'), '): The key of the query parameter.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'String|null'), ': The query parameter or null if not found.'])
]),
]),
_('hr'),
_('h2', 'Request::post($post)'),
_('p', [
'This method will return the post parameter of the request. This will look through the $_POST variable.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'key'), ' (', _('b', 'String'), '): The key of the post parameter.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'String|null'), ': The post parameter or null if not found.'])
]),
]),
_('hr'),
_('h2', 'Request::file($key)'),
_('p', [
'This method will return the file of the request. This will look through the $_FILES variable.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'key'), ' (', _('b', 'String'), '): The key of the file.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'Array|null'), ': The file or null if not found nor valid.'])
]),
]),
];
}
}
class DocBackResponse extends Documentation {
constructor(props) {
super(props);
this.title = 'Response';
this.description = 'When an environment endpoints is called, it should return a response. This class is used to make responses and send them to the client easily.';
this.content = [
_('h2', '\\Ophose\\Response'),
_('p', ['The response is what the server sends to the client. It contains the status code, the headers, the body... Let\'s see how Ophose handle them.']),
_('hr'),
_('h2', 'Attributes'),
_('ul', [
_('li', [_('b', 'body'), ' (', _('b', 'String'), '): The body of the response.']),
_('li', [_('b', 'statusCode'), ' (', _('b', 'int'), '): The status code of the response.']),
_('li', [_('b', 'headers'), ' (', _('b', 'Array'), '): The headers of the response.']),
]),
_('hr'),
_('h2', 'Response($body, $statusCode = 200, $headers = [])'),
_('p', [
'This is the constructor of the response. It will create a response with the given body, status code and headers.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'body'), ' (', _('b', 'String'), '): The body of the response.']),
_('li', [_('b', 'statusCode'), ' (', _('b', 'int'), '): The status code of the response.']),
_('li', [_('b', 'headers'), ' (', _('b', 'Array'), '): The headers of the response.']),
]), _('br'),
]),
_('hr'),
_('h2', 'Response#setStatus($status)'),
_('p', [
'This method will set the status code of the response.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'status'), ' (', _('b', 'int'), '): The status code of the response.']),
]), _('br'),
]),
_('h2', 'Response#setBody($body)'),
_('p', [
'This method will set the body of the response.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'body'), ' (', _('b', 'String'), '): The body of the response.']),
]), _('br'),
]),
_('h2', 'Response#setHeader($key, $value)'),
_('p', [
'This method will set the header of the response.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'key'), ' (', _('b', 'String'), '): The key of the header.']),
_('li', [_('b', 'value'), ' (', _('b', 'String'), '): The value of the header.']),
]), _('br'),
]),
_('h2', 'Response#send()'),
_('p', [
'This method will send the response to the client. It will send the headers and the body.', _('br'),
]),
_('h2', 'Response::raw($body, $statusCode = 200, $headers = [])'),
_('p', [
'This method will create a new response with the given body, status code and headers and send it.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'body'), ' (', _('b', 'String'), '): The body of the response.']),
_('li', [_('b', 'statusCode'), ' (', _('b', 'int'), '): The status code of the response.']),
_('li', [_('b', 'headers'), ' (', _('b', 'Array'), '): The headers of the response.']),
]), _('br'),
]),
_('h2', 'Response::json($data, $statusCode = 200, $headers = [])'),
_('p', [
'This method will create a new response with the given data encoded in JSON, status code and headers and send it.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'data'), ' (', _('b', 'Array'), '): The data to encode in JSON.']),
_('li', [_('b', 'statusCode'), ' (', _('b', 'int'), '): The status code of the response.']),
_('li', [_('b', 'headers'), ' (', _('b', 'Array'), '): The headers of the response.']),
]), _('br'),
]),
_('hr'),
_('h2', 'Response::file($filePath, $statusCode = 200, $headers = [])'),
_('p', [
'This method will create a new response with the given file, status code and headers and send it.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'filePath'), ' (', _('b', 'String'), '): The path of the file.']),
_('li', [_('b', 'statusCode'), ' (', _('b', 'int'), '): The status code of the response.']),
_('li', [_('b', 'headers'), ' (', _('b', 'Array'), '): The headers of the response.']),
]), _('br'),
]),
_('hr'),
_('h2', 'Response::download($filePath, $fileName = null, $statusCode = 200, $headers = [])'),
_('p', [
'This method will create a new response with the given file, status code and headers and send it. It will also set the Content-Disposition header to attachment.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'filePath'), ' (', _('b', 'String'), '): The path of the file.']),
_('li', [_('b', 'fileName'), ' (', _('b', 'String|null'), '): The name of the file (if null it will be the name of the file).']),
_('li', [_('b', 'statusCode'), ' (', _('b', 'int'), '): The status code of the response.']),
_('li', [_('b', 'headers'), ' (', _('b', 'Array'), '): The headers of the response.']),
]), _('br'),
]),
_('hr'),
_('h2', 'Response::redirect($url, $statusCode = 302)'),
_('p', [
'This method will create a new response with the given url and status code and send it. It will also set the Location header to the given url.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'url'), ' (', _('b', 'String'), '): The url to redirect to.']),
_('li', [_('b', 'statusCode'), ' (', _('b', 'int'), '): The status code of the response.']),
]), _('br'),
]),
];
}
}
class DocBackSession extends Documentation {
constructor(props) {
super(props);
this.title = 'Session';
this.description = 'Sessions are used to store data on the server side for a specific client. It is useful to store data such as the logged user...';
this.content = [
_('h2', '\Ophose\Session'),
_('p', ['The Ophose Session class is a wrapper of the PHP session. It allows you to easily handle the session. Note that this class only contains static methods as a session is only sent only on the request. Note that this class handles the session of the current project if the project_id key is set in your project configuration. Meaning that if you have multiple projects on the same server, they will have different sessions to avoid conflicts. Also note that the session is already started when the request is handled.']),
_('hr'),
_('h2', 'Session::get($key, $default = null)'),
_('p', [
'This method will return the session value of the given key.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'key'), ' (', _('b', 'String'), '): The key of the session value.']),
_('li', [_('b', 'default'), ' (', _('b', 'mixed'), '): The default value if the session value is not found.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'mixed'), ': The session value or the default value if not found.'])
]),
]),
_('hr'),
_('h2', 'Session::set($key, $value)'),
_('p', [
'This method will set the session value of the given key.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'key'), ' (', _('b', 'String'), '): The key of the session value.']),
_('li', [_('b', 'value'), ' (', _('b', 'mixed'), '): The value to set.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': Nothing.'])
]),
]),
_('hr'),
_('h2', 'Session::delete($key)'),
_('p', [
'This method will delete the session value of the given key.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'key'), ' (', _('b', 'String'), '): The key of the session value.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': Nothing.'])
]),
]),
_('hr'),
_('h2', 'Session::clear()'),
_('p', [
'This method will clear the session.', _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': Nothing.'])
]),
]),
_('hr'),
_('h2', 'Session::exists($key)'),
_('p', [
'This method will return whether the session value exists or not.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'key'), ' (', _('b', 'String'), '): The key of the session value.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'boolean'), ': Whether the session value exists or not.'])
]),
]),
];
}
}
class DocBackConfiguration extends Documentation {
constructor(props) {
super(props);
this.title = 'Configuration files';
this.description = 'Configuration files are used to configure your environment and your application. Their syntax is the same as JSON and their extension is .oconf.';
this.content = [
_('h2', '\\Ophose\\Configuration'),
_('p', ['This class contains all the static methods you need to interact and get data from your configuration files. Note that when you will get the configuration from a .oconf file with the ', _('b', 'get()'), ' method, the configuration will be loaded then recusively merged with the configuration of the same file with a .oconf.local extension.']),
_('hr'),
_('h2', 'Configuration::get($path)'),
_('p', [
'This method will return the configuration of the given path. Note that the path is relative to the /config folder.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'path'), ' (', _('b', 'String'), '): The path of the configuration file.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'array|null'), ': The configuration of the given path (or null if not found).'])
]),
]),
_('hr'),
_('h2', 'Configuration::import($path)'),
_('p', [
'This method will import the configuration of the given path. Note that the path is relative to the /app/configuration folder.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'path'), ' (', _('b', 'String'), '): The path of the configuration file (without the .oconf extension).'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'array|null'), ': The configuration of the given path (or null if not found).'])
]),
]),
_('hr'),
_('h2', 'Configuration::export($configPath, $inConfigPath)'),
_('p', [
'This method will export the configuration of the given path to the given path. Note that the path is relative to the /app/configuration folder.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'configPath'), ' (', _('b', 'String'), '): The path of the configuration file.']), _('br'),
_('li', [_('b', 'inConfigPath'), ' (', _('b', 'String'), '): The path of the configuration file to export to (relative to /app/configuration and without the .oconf extension).'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'bool'), ': Returns true if the configuration has been exported, otherwise false.'])
]),
]),
];
}
}
class DocBackCookie extends Documentation {
constructor(props) {
super(props);
this.title = 'Cookie';
this.description = 'Cookies are small pieces of data that are stored in the user\'s browser. They are used to store information about the user. This class is a wrapper of the PHP setcookie function and $_COOKIE global variable. It allows you to easily handle cookies. From version: 1.0.0_a7';
this.content = [
_('h2', '\Ophose\Cookie'),
_('p', 'The \Ophose\Cookie class is a wrapper of the PHP setcookie function and $_COOKIE global variable. Note that this class handles the cookies of the current project if the project_id key is set in your project configuration. Meaning that if you have multiple projects on the same server, they will have different cookies to avoid conflicts.'),
_('hr'),
_('h2', 'Cookie::set($name, $value, $expire = 0, $path = \'/\', $domain = \'\', $secure = true, $httponly = false)'),
_('p', [
'This method will set a cookie.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'name'), ' (', _('b', 'String'), '): The name of the cookie.']),
_('li', [_('b', 'value'), ' (', _('b', 'String'), '): The value of the cookie.']),
_('li', [_('b', 'expire'), ' (', _('b', 'int'), '): The expire time (in seconds) of the cookie from now.']),
_('li', [_('b', 'path'), ' (', _('b', 'String'), '): The path on the server in which the cookie will be available on.']),
_('li', [_('b', 'domain'), ' (', _('b', 'String'), '): The domain that the cookie is available to.']),
_('li', [_('b', 'secure'), ' (', _('b', 'bool'), '): Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.']),
_('li', [_('b', 'httponly'), ' (', _('b', 'bool'), '): When TRUE the cookie will be made accessible only through the HTTP protocol.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': Nothing.'])
]),
]),
_('hr'),
_('h2', 'Cookie::get($name, $default = null)'),
_('p', [
'This method will return the cookie value of the given name.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'name'), ' (', _('b', 'String'), '): The name of the cookie.']),
_('li', [_('b', 'default'), ' (', _('b', 'mixed'), '): The default value if the cookie is not found.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'mixed'), ': The cookie value or the default value if not found.'])
]),
]),
_('hr'),
_('h2', 'Cookie::delete($name)'),
_('p', [
'This method will delete the cookie of the given name.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'name'), ' (', _('b', 'String'), '): The name of the cookie.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': Nothing.'])
]),
]),
_('hr'),
_('h2', 'Cookie::has($name)'),
_('p', [
'This method will check if the cookie of the given name exists.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'name'), ' (', _('b', 'String'), '): The name of the cookie.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'bool'), ': TRUE if the cookie exists, FALSE otherwise.'])
]),
]),
_('hr'),
_('h2', 'Cookie::all()'),
_('p', [
'This method will return all the cookies.', _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'array'), ': An associative array containing all the cookies.'])
]),
]),
_('hr'),
_('h2', 'Cookie::clear()'),
_('p', [
'This method will clear all the cookies.', _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': Nothing.'])
]),
])
];
}
}
class DocBackWith extends Documentation {
constructor(props) {
super(props);
this.title = 'With';
this.description = 'With is a class allowing you to call function or callback if a condition is validated or run a default function is the condition is false. From version: 1.0.0_a8';
this.content = [
_('h2', '\Ophose\With'),
_('p', 'The \Ophose\With class may be really useful when you want to run a function or callback if a condition is validated or run a default function is the condition is false. It is a simple way to avoid multiple if/else statements. For example, you can check if a user is logged in and run a function if it is the case or run a default function if it is not (like redirecting to the login page).'),
_('hr'),
_('h2', 'With::condition($condition)'),
_('p', [
'This method will set the condition to validate.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'condition'), ' (', _('b', 'bool'), '): The condition to validate.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'With'), ': The instance that has been created.'])
]),
]),
_('hr'),
_('h2', 'With#send(...$arguments)'),
_('p', [
'This method will send the arguments to the function or callback if the condition is validated.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '...$arguments'), ' (', _('b', 'mixed'), '): The arguments to send to the function or callback. If the function or callback does not accept any arguments, you can omit this parameter.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'With'), ': The instance that has been created.'])
]),
]),
_('hr'),
_('h2', 'With#otherwise($function)'),
_('p', [
'This method will set the default function to run if the condition is false.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'function'), ' (', _('b', 'callable'), '): The default function to run if the condition is false.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'With'), ': The instance that has been created.'])
]),
]),
_('hr'),
_('h2', 'With#go($callback)'),
_('p', [
'This method will run the function or callback if the condition is validated or run the default function if the condition is false.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', 'callback'), ' (', _('b', 'callable'), '): The function or callback to run.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'mixed'), ': The result of the function or callback.'])
]),
]),
];
}
}
class DocBackUtil extends Documentation {
constructor(props) {
super(props);
this.title = 'Util';
this.description = 'This is a collection of utility methods that can be used in your Ophose application.';
this.content = [
_('h2', 'Util'),
_('h2', 'o_get_files_recursive($dir, $ext = null, max_recursion = 512)'),
_('p', [
'This method will return an array of files in a directory and its subdirectories.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$dir'), ' (', _('b', 'string'), '): The directory path.']), _('br'),
_('li', [_('b', '$ext'), ' (', _('b', 'string'), '): The file extension to filter.']), _('br'),
_('li', [_('b', 'max_recursion'), ' (', _('b', 'int'), '): The maximum recursion level.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'array'), ': The array of files.'])
]),
]),
_('hr'),
_('h2', 'o_rm_dir_recursive($dir)'),
_('p', [
'This method will remove a directory and its content.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$dir'), ' (', _('b', 'string'), '): The directory path.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'bool'), ': Returns true if the directory has been removed, otherwise false.'])
]),
]),
_('hr'),
_('h2', 'o_between(&$value, $min, $max)'),
_('p', [
'This method will check if a value is between two others and modify it to be in the range.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$value'), ' (', _('b', 'int'), '): The value to check and modify.']), _('br'),
_('li', [_('b', '$min'), ' (', _('b', 'int'), '): The minimum value.']), _('br'),
_('li', [_('b', '$max'), ' (', _('b', 'int'), '): The maximum value.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': The value will be modified directly.'])
]),
])
];
}
}
class DocBackApp extends Documentation {
constructor(props) {
super(props);
this.title = 'App';
this.description = 'App is a class allowing you to easily manage your app resources such as configuration, assets, templates... From version: 1.0.0_a9';
this.content = [
_('h2', '\Ophose\App'),
_('p', 'The \Ophose\App class is used when you want to manage your app files, configurations and even more... For example, you may want to save your configuration files for all your environments (development, production...). '),
_('hr'),
_('h2', 'App::getAppPath($inAppPath = "")'),
_('p', [
'This method will return the path of the app directory or a file in the app directory.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$inAppPath'), ' (', _('b', 'string'), '): The file path in the app directory.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'string'), ': The path of the app directory or the file in the app directory.'])
]),
]),
_('hr'),
_('h2', 'App::export($assetPath, $inAssetPath, bool $overwrite = false)'),
_('p', [
'This method will export an asset file to the app directory.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$assetPath'), ' (', _('b', 'string'), '): The asset file path.']), _('br'),
_('li', [_('b', '$inAssetPath'), ' (', _('b', 'string'), '): The file path in the app directory.']), _('br'),
_('li', [_('b', '$overwrite'), ' (', _('b', 'bool'), '): If the file already exists, should it be overwritten?'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'bool'), ': Returns the file path in the app directory or false if the file already exists and the overwrite parameter is false.'])
]),
]),
];
}
}
class DocBackTemplate extends Documentation {
constructor(props) {
super(props);
this.title = 'Template';
this.description = 'Template is a classe to easily create template for your Ophose application, for example you can imagine templates for mails and more... From version: 1.0.0_a9';
this.content = [
_('h2', "\\Ophose\\Template"),
_('p', 'The \\Ophose\\Template class is used when you want to create a template for your application. For example, you may want to create a template for your emails or for your views... '),
_('hr'),
_('h1', 'Example: '), _('br'),
new CodeBlock({
lang: 'html',
lines: [
'',
'',
'',
'',
'',
' My Template',
' ',
'',
'',
' = $title ?>
',
' = $content ?>
',
'',
]
}),
_('hr'),
_('h2', 'Template::render($template, $data = [])'),
_('p', [
'This method will render a template with the given data.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$template'), ' (', _('b', 'string'), '): The template file path (relative to /app/templates/). Note that you can use "." to go in sub folders']), _('br'),
_('li', [_('b', '$data'), ' (', _('b', 'array'), '): The data to pass to the template. You will be able to use these data in the template directly writing the key name. For example, if you pass an array with a key "title" you will be able to use it in the template with the variable $title.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'string'), ': The rendered template.'])
]),
]),
_('hr'),
_('h2', 'Template::getTemplatePath($template)'),
_('p', [
'This method will return the path of the template file.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$template'), ' (', _('b', 'string'), '): The template file path (relative to /app/templates/). Note that you can use "." to go in sub folders'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'string'), ': The path of the template file.'])
]),
]),
_('hr'),
_('h2', 'Template::templateExists($template)'),
_('p', [
'This method will check if the template file exists.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$template'), ' (', _('b', 'string'), '): The template file path (relative to /app/templates/). Note that you can use "." to go in sub folders'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'bool'), ': True if the template exists, false otherwise.'])
]),
]),
_('hr'),
_('h2', 'Template::extends($template)'),
_('p', [
'This method will extend a template.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$template'), ' (', _('b', 'string'), '): The template file path (relative to /app/templates/). Note that you can use "." to go in sub folders'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': The template will be extended.'])
]),
]),
_('h2', 'Template::section($section)'),
_('p', [
'This method will include a section of the child template.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$section'), ' (', _('b', 'string'), '): The section name.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': The section will be included in the template.'])
]),
]),
_('hr'),
_('h2', 'Template::inSection($section)'),
_('p', [
'This method will start a section of the template. You may put your HTML/PHP code in it.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$section'), ' (', _('b', 'string'), '): The section name.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': The section will be started.'])
]),
]),
_('h2', 'Template::endSection($section)'),
_('p', [
'This method will end a section of the template. You may put your HTML/PHP code in it.', _('br'),
_('u', 'Parameters: '),
_('ul', [
_('li', [_('b', '$section'), ' (', _('b', 'string'), '): The section name.'])
]), _('br'),
_('u', 'Returns: '),
_('ul', [
_('li', [_('b', 'void'), ': The section will be ended.'])
]),
]),
];
}
};
;
class DocCommandBuild extends Documentation {
constructor(props) {
super(props);
this.title = 'Build command';
this.description = 'The build command is used to build the application for production. It will compile the application and ophose itself into app.js and ophose.js in the public folder.';
this.content = [
_('h2', 'Command'),
_('p', ['To run the build command, you need to run the following command:']),
new CodeBlock({lines: [
'php ocl oph build'
]}),
_('p', ['This command will compile the application and ophose itself into app.js and ophose.js in the public folder. Don\'t forget to switch the environment production mode in the project.oconf file to true to use them.']),
];
}
};
;
class DocCommandInstall extends Documentation {
constructor(props) {
super(props);
this.title = 'Install command';
this.description = 'The install command is used to install the dependencies of the application. It will install the dependencies of the application.';
this.content = [
_('h2', 'Command'),
_('p', ['To run the install command, you need to run the following command:']),
_('h3', 'Options'),
_('ul',
_('li', _('b', '-u:'), 'Remove the dependencies and install the new ones.')
),
new CodeBlock({lines: [
'php ocl oph install'
]}),
_('p', ['This command will install the dependencies of your application. Don\'t forget to run this command after cloning the project from the repository and put your API key in the project.oconf file.'])
];
}
};
;
class DocCommandTrigger extends Documentation {
constructor(props) {
super(props);
this.title = 'Trigger command';
this.description = 'The trigger command is used to trigger the specified event for any environment. It will trigger the specified event for any environment.';
this.content = [
_('h2', 'Command'),
_('p', ['To run the trigger command, you need to run the following command:']),
new CodeBlock({
lines: [
'php ocl oph trigger '
]
}),
_('h3', 'Options'),
_('ul',
_('li', _('b', ':'), 'The environment to trigger the event.'),
_('li', _('b', ':'), 'The event to trigger.')
),
_('h3', 'Triggers:'),
_('ul',
_('li', _('b', 'install:'), 'Trigger the install event.'),
),
_('p', ['This command will trigger the specified event for any environment. Don\'t forget that if you want to trigger an event of an installed environment, you need to specify the environment as :.'])
];
}
};
;
class DocCommandUpdate extends Documentation {
constructor(props) {
super(props);
this.title = 'Update command';
this.description = 'The update command is used to install the specified version of Ophose (from 1.0.0_c).';
this.content = [
_('h2', 'Command'),
_('p', ['To run the update command, you need to run the following command:']),
new CodeBlock({lines: [
'php ocl oph update'
]}),
_('p', ['This command will install the specified version of Ophose. Don\'t forget to run this command after cloning the project from the repository and put your Ophose version in the project.oconf file.'])
];
}
}class IndexIntro extends Ophose.Component {
constructor() {
super();
}
style() {
return /*css*/`
%self {
background-color: var(--bg-color);
width: 100%;
max-width: 100%;
height: 600px;
position: relative;
}
%self .content {
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
align-content: center;
flex-wrap: nowrap;
overflow: hidden;
width: 100%;
height: 100%;
gap: 1rem;
}
%self h3 {
font-size: 2rem;
font-weight: 400;
color: var(--text-color-light);
}
%self .content > h4 {
width: 60%;
margin: 0 auto;
}
%self h1 {
font-size: 7rem;
background: var(--gradient-main);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
%self p {
font-size: 1.25rem;
font-weight: 400;
color: var(--text-color-light);
width: 60%;
margin: 0 auto;
}
%self a.alert {
margin: 1rem 0;
font-size: 0.75rem;
font-weight: 400;
}
%self .phones {
position: absolute;
top: 50%;
right: 0;
width: 25%;
transform: translateY(-50%);
object-fit: contain;
}
%self .stats {
margin-top: 4rem;
justify-content: center;
gap: 4rem;
}
%self .stats h4 {
font-size: 1.5rem;
font-weight: 800;
}
%self .stats h4 span {
color: var(--text-color-light);
font-weight: 400;
text-transform: uppercase;
font-size: 0.65em;
}
`
}
styles() {
return {
'md': /*css*/`
%self {
height: 100vh;
}
%self .content {
gap: 0.5rem;
}
%self h3 {
font-size: 1.5rem;
}
%self h1 {
font-size: 4rem;
}
%self .stats {
flex-direction: column;
font-size: 0.75rem;
gap: 1rem;
}
%self p {
width: 90%;
font-size: 1rem;
}
`
}
}
render() {
return {_: 'section', c: [
{_: 'div', className: 'wrapper content', c: [
_('h3', 'AH4 introduces you,'),
_('h1', 'OPHOSE'),
_('h4', 'The open-source web framework for building your next web app you dream about.'),
_('p', 'Ophose is a web framework that has been built and designed to be simple, fast, easy to use and as lightweight and scalable as possible. It is built on top of JavaScript and PHP, and is open-source.'),
new PlacedLive(appLives.STATS, (stats) => {
if(!stats) return _('div', 'Loading stats...');
return _('div', {className: 'row stats fade-in'}, [
_('h4', [stats.users, _('span', ' users')]),
_('h4', ['$', stats.shared_revenue.toFixed(2), _('span', ' shared revenue')]),
_('h4', [stats.downloads, _('span', ' downloads')])
])
})
]}
]}
}
}class IndexCraft extends Ophose.Component {
constructor() {
super();
}
style() {
return /* css */`
%self {
background-color: var(--bg-color-light);
border-top: var(--border-light);
}
%self .content {
display: flex;
align-items: center;
justify-content: center;
padding: 4rem 0;
gap: 3rem;
}
%self .content > div {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
flex: 1;
}
%self h1 {
font-size: 3rem;
}
%self p {
font-size: 1.2rem;
font-weight: 300;
}
%self a {
font-size: 1.2rem;
font-weight: 300;
}
/* Arg */
%self .arg {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
padding: 0.5rem;
border-radius: 0.25em;
transition: 0.2s;
user-select: none;
}
%self .arg i {
font-size: 2rem;
padding: 0.75rem;
}
%self .arg .title {
font-size: 1.2rem;
font-weight: 400;
}
%self .arg .desc {
font-size: 1rem;
font-weight: 300;
color: var(--text-color-light);
}
/* Args */
%self .args {
display: flex;
flex-direction: row;
gap: 4rem;
}
`;
}
styles() {
return {
'md': /* css */`
%self .content {
flex-direction: column;
}
%self h1 {
font-size: 2rem;
}
`
}
}
render() {
let arg = (icon, title, desc) => {
return _('div', {className: 'arg'}, [
_('i', {className: 'bi bi-' + icon}),
_('div', [
_('b', {className: 'title'}, title),
_('p', {className: 'desc'}, desc)
])
])
}
return {_: 'section', c: [
{_: 'div', className: 'wrapper content', c: [
_('div', [
_('h1', 'Easily craft and scale your web app as you needs'),
_('p', 'Ophose is a modern and simple web framework for building web apps with the power of web components. It is designed to be simple, fast and easy to use. You may design your own components and use them in your app.'),
new Link({className: 'button', href: '/tutorials/get-started', c: 'Get started with Ophose'})
]),
_('div', {className: 'args'}, [
arg('front', 'Front-end', 'Your front-end can be crafted really easily and fast with Ophose components in Javascript. You may design your owns and use them in your app.'),
arg('back', 'Back-end', 'To create the logic and the back-end of your app, you will use environments. An environment is a module of your application and can communicate with other environments. For example: Database, Auth, Storage...'),
arg('box', 'Scale', 'Ophose is designed to be easily scalable. You can create your own environments and use them in your app. You can also create your own components and use them in your app.')
]),
]}
]}
}
};
class IndexExamplePost extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
background-color: var(--comp-bg-color);
border: 1px solid var(--comp-border-color);
border-radius: 1rem;
font-family: var(--font-sen);
font-size: 0.75em;
}
%self h2 {
font-size: 1.25rem;
font-weight: 400;
border-bottom: var(--border-light);
padding: 0.75rem;
}
%self p {
font-weight: 400;
color: var(--text-color-light);
padding: 0.75rem;
letter-spacing: 0.05rem;
border-bottom: var(--border-light);
}
%self .actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
padding: 0.75rem;
}
%self .actions > * {
font-size: 1rem;
color: var(--text-color-light);
cursor: pointer;
transition: color 0.2s;
display: block;
}
%self .actions > *:hover {
color: var(--text-color);
margin: 0 0.25rem;
}
`
}
render() {
return {_: 'div', children: [
{_: 'h2', children: this.props.title},
{_: 'p', children: this.props.content},
{_: 'div', className: 'actions', onclick: () => alert(`You clicked on a post action of ${this.props.author}!`), children: [
{_: 'a', className: 'bi bi-heart'},
{_: 'a', className: 'bi bi-pencil'},
{_: 'a', className: 'bi bi-trash'},
]}
]}
}
}
class IndexExample extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
border-top: var(--border-light);
padding: 4rem 0;
}
%self h1 {
font-size: 3rem;
text-align: center;
width: 70%;
margin: 0 auto;
}
%self .desc {
font-size: 1.5rem;
text-align: center;
width: 80%;
margin: 2rem auto;
color: var(--text-color-light);
}
%self .post-content {
width: 100%;
margin: 0 auto;
display: flex;
gap: 1rem;
align-items: center;
position: relative;
}
%self .post-content > * {
flex: 1;
}
%self .supa {
position: absolute;
top: 10%;
left: 5%;
border-radius: 4em;
transform: translate(-2.5%, -5%);
width: 95%;
max-width: 95%;
height: 90%;
background: var(--gradient-main);
opacity: 0.333;
z-index: -1;
}
%self .video {
width: 100%;
height: 700px;
margin: 2rem 0;
border-radius: 1rem;
border: none;
box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 24px;
}
`
}
styles() {
return {
'md': /* css */`
%self .post-content {
flex-direction: column;
}
%self pre {
width: 100%;
overflow-x: auto;
padding: 1rem;
border-radius: 1rem;
font-size: 0.75rem;
margin: 1rem 0;
}
%self h1 {
font-size: 2rem;
}
`
}
}
render() {
return {
_: 'section', className: 'wrapper', children: [
_('h1', 'Discover a new way to implement data in your components'),
_('p', {className: 'desc'}, 'Ophose is a framework that allows you to create components with data and events in a simple and fast way. Let\'s see an example of how to use it with a post:'),
_('div', { className: 'post-content' }, [
_('div', { className: 'supa' }),
new CodeBlock({lines: [
"// Render method in class Post (Post.js)",
"render() {",
" return {",
" _: 'div', children: [",
" _('h2', this.props.title),",
" _('p', this.props.content),",
" new PostActions({",
" onclick: () => alert(`You clicked on a post action of ${this.props.author}!`)",
" })",
" ]",
" }",
"}"
]}),
new IndexExamplePost({
title: 'Welcome on Ophose!',
content: 'See how easy it is to create a component with Ophose! It\'ll transform your JS Objects and Ophose components into HTML elements. _ stands for the tag name, and children (or c) stands for the children of the element as in DOM structure. You can also use the props of the component in the render method, as you can see in the example and sub components. Finally you may place components in other components.',
author: 'AH4'
})
]),
_('iframe', {className: 'video', width: "560", height: "315", src: "https://www.youtube.com/embed/7GFghTu4_R8?si=paKY7olKqyIKXdH", title: "YouTube video player", frameborder: "0", allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowfullscreen: true})
]
}
}
}class IndexFeatures extends Ophose.Component {
constructor() {
super();
}
style() {
return /* css */`
%self {
border-top: var(--border-light);
}
%self .content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8rem 0;
gap: 1rem;
}
%self h1 {
font-size: 4rem;
background: var(--gradient-main);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
width: 70%;
text-align: center;
}
%self .content > p {
font-size: 1.25rem;
font-weight: 300;
width: 60%;
text-align: center;
}
/* Features */
%self .features {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-left: auto;
text-align: left;
}
%self .feature {
padding: 1rem;
width: 33.333%;
position: relative;
}
%self .feature_content {
display: flex;
flex-direction: column;
padding: 1.5rem;
gap: 1rem;
width: 100%;
height: 100%;
border: var(--comp-border);
border-radius: 0.5em;
transition: 0.2s;
user-select: none;
}
%self .feature_content:hover {
box-shadow: 0 0 10px rgba(127, 127, 127, 0.1);
}
%self .feature_content i {
font-size: 1rem;
}
%self .feature_content h4 {
font-size: 1.5rem;
font-weight: 400;
}
%self .feature_content p {
font-size: 0.8rem;
font-weight: 300;
color: var(--text-color-light);
}
`;
}
styles() {
return {
'md': /* css */`
%self h1 {
font-size: 2rem;
width: 100%;
}
%self .content > p {
width: 80%;
}
%self .features {
flex-direction: column;
text-align: center;
}
%self .feature {
width: 100%;
margin-bottom: 1rem;
}
`
}
}
render() {
let feature = (icon, title, description) => {
return _('div', {className: 'feature'}, [
_('div', {className: 'feature_content'}, [
_('i', {className: 'bi bi-' + icon}),
_('h4', title),
_('p', description)
])
])
}
return {_: 'section', id: 'features', c: [
{_: 'div', className: 'wrapper content', c: [
_('h1', 'This framework already has everything you need'),
_('p', 'With its integrated features, Ophose allows you to create your website in only a few minutes. For example it has a front dynamic router allowing you to navigate between pages without reloading the page and more...'),
_('div', {className: 'features'}, [
feature('link', 'Dynamic router', 'The dynamic router allows you to navigate between pages without reloading the page and unsertands dynamic pages such as /user/{id} for example.'),
feature('bricks', 'Components', 'It has a system of components allowing you to easily create your own components and re-use them as you need.'),
feature('box', 'Scalable', 'The framework is fully customizable and has been made to be as scalable as you need. The only limit is your imagination.'),
feature('hand-thumbs-up', 'Easy to use', 'The framework is very easy to use, you can create your website in a few minutes and it requires you only a basic knowledge of JS and PHP.'),
feature('cpu', 'Fast', 'Ophose is very fast, for your server because it is mainly used as Rest API and for UI because it loads asynchronously.'),
feature('github', 'Open source', 'It is open source, you can contribute to the project and help us to improve it and even create your own version.'),
feature('hearts', 'Free', 'It\'s free, you can use it for your personal or commercial projects. Please make sure to credit us if you use it for commercial projects. You can see the license in the Github repository.'),
feature('lightning', 'Lightweight', 'Ophose is very lightweight and portable, it is less 10MB and it is very easy to install and use.'),
feature('shield', 'Secure', 'It is mostly secured, it has a system of protection against XSS and CSRF attacks and you don\'t even have to worry about hackers anymore.')
])
]}
]}
}
};
class IndexBridge extends Ophose.Component {
constructor(props) {
super(props);
this.displayMessage = new Live('No message yet');
}
style() {
return /* css */`
%self {
border-top: var(--border-light);
padding: 4rem 0;
}
%self h1 {
font-size: 3rem;
text-align: center;
width: 70%;
margin: 0 auto;
}
%self .desc {
font-size: 1.5rem;
text-align: center;
width: 80%;
margin: 2rem auto;
color: var(--text-color-light);
}
%self .post-content {
width: 100%;
margin: 0 auto;
display: flex;
flex-direction: row-reverse;
gap: 1rem;
align-items: center;
position: relative;
}
%self .post-content > * {
flex: 1;
}
%self .supa {
position: absolute;
top: 10%;
right: 5%;
border-radius: 4em;
transform: translate(-2.5%, -5%);
width: 95%;
height: 90%;
background: var(--gradient-main);
opacity: 0.333;
z-index: -1;
}
/* fetcher */
%self .fetcher {
display: flex;
flex-direction: column;
gap: 1rem;
background-color: var(--bg-color);
border-radius: 1em;
padding: 1rem;
}
`
}
styles() {
return {
'md': /* css */`
%self .post-content {
flex-direction: column;
}
%self .post-content > * {
flex: 1;
max-width: 100%;
}
%self .supa {
display: none;
}
%self .fetcher {
padding: 1.5rem;
}
`
}
}
render() {
return {
_: 'section', className: 'wrapper', children: [
_('h1', 'Easily create a bridge between your frontend and backend'),
_('p', {className: 'desc'}, 'This framework has been designed to be fullstack and to make it easy and fast creating a bridge between your frontend and backend. You can create endpoints in your backend that you\'ll be able to call.'),
_('div', { className: 'post-content' }, [
_('div', { className: 'supa' }),
new CodeBlock({lines: [
"// Creating an endpoint in your environment Tutorial (env.php)",
'endpoint("hello", function() {"',
' // This endpoint will be accessible at /@/tutorial/hello',
' Response::json([',
' "message" => "Hello World! It is " . date("H:i:s")',
' ]);',
' });',
' }',
'',
'}'
], language: 'php'}),
_('div', { className: 'fetcher' }, [
_('p', ['You can now fetch your endpoint data using the async ', _('b', 'oenv'), ' method.']),
new CodeBlock({lines: [
"oenv('tutorial/hello')",
" .then(data => {",
" this.displayMessage.set(data.message);",
" };"
]}),
_('p', ['The last fetch message is: ', _('b', this.displayMessage)]),
_('button', {onclick: () => {
oenv('tutorial/hello')
.then(data => {
this.displayMessage.set(data.message);
});
}, className: 'button'}, 'Click here to fetch the endpoint')
]),
])
]
}
}
}class ProfileHeader extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
@keyframes animateGradient {
0%{background-position:0% 50%}
50%{background-position:100% 50%}
100%{background-position:0% 50%}
}
%self {
padding: 2rem;
display: flex;
flex-direction: row;
gap: 2rem;
align-items: center;
background: linear-gradient(104deg, rgba(102,126,234,0.1) 0%, rgba(126,102,234,0.1) 50%, rgba(234,102,126,0.1) 100%);
background-size: 400% 400%;
animation: animateGradient 5s ease infinite;
}
%self .pfp {
width: 10rem;
height: 10rem;
border-radius: 50%;
}
%self .info {
display: flex;
flex-direction: column;
gap: 1rem;
}
%self .info h2 {
font-size: 1.5rem;
padding: 0.25em 0.75em;
background-color: var(--comp-bg-color);
border-radius: 1em;
width: fit-content;
}
%self .info .bio {
color: var(--text-color-light);
}
`
}
render() {
return {_: 'div', children: [
_('img', {src: '/@/ah4/user/image', className: 'pfp'}),
new PlacedLive(appLives.USER, (user) => {
if(!user) return {_: 'div', className: 'info', children: [
_('h2', 'Loading...'),
_('p', {className: 'bio'}, 'Loading...')
]}
return {_: 'div', className: 'info', children: [
_('h2', user.username),
_('p', {className: 'bio'}, user.bio)
]}
})
]}
}
}class MessageBox extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /*css*/`
%self .hider {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(5px);
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
%self .message-box {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--comp-bg-color);
border-radius: 1em;
padding: 2em;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
z-index: 1001;
width: 30%;
}
%self .message-box h1 {
font-size: 1.5em;
margin-bottom: 1em;
}
%self .message-box .message-content {
margin-bottom: 1em;
}
%self .message-box .message-buttons {
display: flex;
flex-direction: row;
gap: 1em;
width: 100%;
position: relative;
}
%self .message-box .message-button {
flex: 1;
cursor: pointer;
user-select: none;
display: block;
padding: 0.5em 1em;
border-radius: 2em;
background-color: var(--link-color);
transition-duration: 0.25s;
text-align: center;
font-weight: bold;
}
%self .message-box .message-button:hover {
transform: scale(1.025);
}
`
}
onClose() {
if (this.props.onClose) this.props.onClose();
this.remove();
}
render() {
return {
_: 'div', children: [
{ _: 'div', className: 'hider', onclick: () => { this.onClose() } },
{_: 'div', className: 'message-box', children: [
{ _: 'h1', text: this.props.title },
{ _: 'div', className: 'message-content', children: this.props.children },
_('div', { className: 'message-buttons' },
this.props.cancellable && { _: 'a', className: 'message-button', style: 'background-color: var(--comp-bg-color);', text: 'Cancel', onclick: () => {
this.onClose();
} },
{ _: 'a', className: 'message-button', text: 'Ok', onclick: () => {
this.onClose();
this.props.onOk();
} }
)
]}
]
}
}
};
class ApiKey extends Ophose.Component {
constructor(props) {
super(props);
this.visible = new Live(false);
this.key = new Live(null);
oenv('ah4/user/api_key')
.then((r) => {
this.key.set(r.api_key);
});
}
style() {
return /* css */`
%self {
background-color: var(--comp-bg-color);
padding: 1rem;
border-radius: 0.5rem;
border: var(--comp-border);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
%self .desc {
color: var(--text-color-light);
}
%self .key {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
font-family: monospace;
font-size: 1.25rem;
}
%self .key > div {
display: flex;
flex-direction: row;
gap: 1rem;
}
%self .key a {
color: var(--text-color-light);
font-size: 1.5rem;
}
%self .button {
white-space: nowrap;
}
%self .key p {
width: -webkit-fill-available;
overflow-x: hidden;
padding: 0.5rem;
border-radius: 2rem;
background-color: rgba(127, 127, 127, 0.125);
}
`
}
styles() {
return {
'md': /* css */`
%self {
padding: 2rem;
}
%self .key {
flex-direction: column;
gap: 1rem;
}
`
}
}
render() {
return {_: 'div', children: [
_('p', {className: 'desc'}, 'Your API key'),
_('div', {className: 'key'},
_('a', {className: 'bi bi-key'}),
_('p', new PlacedLive(this.visible, this.key, (v, k) => {
if(k === null) return 'Loading...';
return v ? k : '•'.repeat(k.length);
})),
new PlacedLive(this.key, (key) => {
return key && _('div', {className: 'actions'},
_('button', {className: 'button', onclick: () => {
navigator.clipboard.writeText(key);
Toast.show({
title: 'Copied',
message: 'API key copied to clipboard'
});
}}, 'Copy'),
_('button', {className: 'button', onclick: () => {this.visible.toggle()}}, 'Toggle visibility'),
_('button', {className: 'button', style: 'background-color: #f44; color: white;', onclick: () => {
this.appendChild(new MessageBox({
title: 'Generate new key',
cancellable: true,
children: _('div', {style: 'display: flex; flex-direction: column; gap: 1rem;'},
_('p', 'Are you sure you want to generate a new API key?'),
_('p', 'This will invalidate your current key and you will have to update all your API clients.'),
_('p', 'This action is irreversible.')
),
onOk: () => {
oenv('ah4/user/api_key', {
options: {
method: 'DELETE'
}
})
.then((r) => this.key.set(r.api_key));
}
}));
}}, 'Generate new key'),
);
})
)
]}
}
};
class MyAPI extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
}
`
}
render() {
return {_: 'div', className: 'my', children: [
_('h1', 'API'),
_('p', {className: 'description'}, 'You\'re on your API page. This is where you can find and manage your API key and other API related stuff. This will allow you to use the Ophose API to fetch components and environments for your project and more.'),
new ApiKey()
]}
}
};
;
class MySettings extends Ophose.Component {
constructor(props) {
super(props);
this.data = new Live(undefined);
oenv('ah4/user/profile')
.then((data) => {
this.data.set(data);
});
}
style() {
return /* css */`
%self {
display: flex;
flex-direction: column;
gap: 2rem;
}
%self > p {
color: var(--text-color-light);
}
`
}
render() {
return {_: 'div', children: [
_('h1', 'Settings'),
_('p', 'You can change your details, password and more here.'),
_('hr'),
new PlacedLive(this.data, (data) => {
return data ? new XForm({
className: 'form',
endpoint: 'ah4/user/change_details',
autocomplete: 'off',
onSuccess: () => {
Toast.show({
title: 'Success',
message: 'Your details have been changed.'
});
},
children: [
_('h2', 'Change your details'),
_('p', 'You can change your details here.'),
_('div', {className: 'row', style: 'gap: 1rem; align-items: flex-start; width: 100%;'},
new XInput({
name: 'email',
type: 'email',
label: 'Email',
placeholder: data.email,
autocomplete: "off",
style: 'flex: 2;'
}),
new XInput({
name: 'new_password',
type: 'password',
label: 'New password',
autocomplete: "new-password",
style: 'flex: 1;'
}),
new XInput({
name: 'new_password_confirm',
type: 'password',
label: 'Confirm new password',
autocomplete: "new-password",
style: 'flex: 1;'
}),
),
_('div', {className: 'row', style: 'gap: 1rem; align-items: flex-start; width: 100%;'},
new XInput({
name: 'paypal_email',
type: 'email',
label: 'PayPal email',
placeholder: data.paypal_email,
autocomplete: "off",
style: 'flex: 1;'
}),
),
_('div', {className: 'row_group'},
_('button', {type: 'submit', className: 'button', style: 'flex: 1;'}, 'Change details'),
),
]
}) : _('h2', 'Loading...');
}),
_('h2', 'Change your profile picture'),
_('p', 'You can change your profile picture here.'),
new XForm({endpoint: 'ah4/user/change_profile_picture', className: 'form', onSuccess: () => {
Toast.show({
title: 'Success',
message: 'Your profile picture has been changed.'
});
}, children: [
new XInput({type: 'file', name: 'image', required: true, label: 'Profile picture'}),
_('button', {type: 'submit', className: 'button'}, 'Change profile picture')
]})
]}
}
}class Resource extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
border-radius: 0.75rem;
border: var(--comp-border);
margin-bottom: 1rem;
width: 16.6666%;
}
%self img {
width: 100%;
height: 100%;
object-fit: cover;
aspect-ratio: 1 / 1;
border-radius: 2em;
background-color: var(--comp-bg-color);
display: flex;
align-items: center;
justify-content: center;
}
%self .info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
%self .info h2 {
font-size: 1rem;
font-weight: 600;
margin: 0;
}
%self .info h2 sup {
font-size: 0.75rem;
font-weight: 400;
color: var(--text-color-light);
}
%self .info p {
font-size: 0.75rem;
}
%self .info .price {
color: var(--text-color-light);
}
`
}
styles() {
return {
'md': /* css */`
%self {
width: 100%;
}
`
}
}
render() {
return new Link({className: 'item', href: '/resource/' + this.props.id, children: [
{_: 'img', src: this.props.imageUrl, alt: this.props.name},
{_: 'div', className: 'info', children: [
_('h2',
this.props.name,
this.props.rating.average !== 0 && _('sup', ' ', this.props.rating.average.toFixed(2) ,' ⭐️ (', this.props.rating.count, ')')
),
{_: 'p', text: this.props.owner},
{_: 'p', className: 'price', text: this.props.price ? '$' + this.props.price : 'Free'},
{_: 'p', text: this.props.resourceType},
]}
]})
}
};
;
class ResourceList extends Ophose.Component {
constructor(props) {
super(props);
this.fetchPoint = this.props.fetchPoint;
this.data = new Live(null);
this.currentPage = new Live(1);
this.maxPages = new Live(1);
this.amountPerPage = 25;
this.searchValue = new Live('');
this.sortValue = new Live('date');
this.lastChanged = Date.now();
this.onChange();
}
onChange = () => {
this.lastChanged = Date.now();
this.data.set(null);
setTimeout(() => {
if(Date.now() - this.lastChanged < 500) return;
oenv(this.fetchPoint, {
search: this.searchValue.get(),
sort: this.sortValue.get(),
offset: (this.currentPage.get() - 1) * this.amountPerPage,
limit: this.amountPerPage,
options: {
toFormData: true
}
})
.then(r => {
this.maxPages.set(Math.ceil(r.total / this.amountPerPage));
if(this.maxPages.get() === 0) this.maxPages.set(1);
this.data.set(r);
});
}, 500)
};
style() {
return /* css */`
%self {
padding: 2em 0;
}
%self .resources {
display: flex;
flex-wrap: wrap;
gap: 1rem;
width: 100%;
position: relative;
flex-wrap: wrap;
padding: 2em 0;
}
%self .controls {
display: flex;
gap: 1rem;
width: 100%;
position: relative;
flex-wrap: wrap;
padding: 2em 0;
justify-content: space-between;
}
`
}
styles() {
return {
'md': /* css */`
%self .resources {
gap: 2rem;
flex-direction: column;
}
%self .controls {
flex-direction: column;
gap: 1rem;
}
%self .controls_page {
flex-direction: column;
gap: 1rem;
}
`
}
}
render() {
return {_: 'div', children: [
_('div', {className: 'controls'}, [
_('input', {type: 'text', placeholder: 'Search', oninput: (e) => {
this.searchValue.set(e.target.value);
this.onChange();
}, className: 'input search'}),
_('div', {className: 'controls_page', style: 'display: flex; align-items: center; gap: 1em;'}, [
_('a', {className: 'button', onclick: () => {
let current = this.currentPage.get();
if(current === 1) return;
this.currentPage.set(current - 1);
this.onChange();
}}, '<'),
_('a', {type: 'text', className: 'input'}, [
'Page ',
this.currentPage,
'/',
this.maxPages
]),
_('a', {className: 'button', onclick: () => {
let current = this.currentPage.get();
if(current === this.maxPages.get()) return;
this.currentPage.set(current + 1);
this.onChange();
}}, '>'),
_('select', {className: 'comp', onchange: (e) => {
this.sortValue.set(e.target.value);
this.onChange();
}, value: 'date'}, [
_('option', {value: 'date'}, 'Sort by: Date'),
_('option', {value: 'downloads'}, 'Sort by: Downloads'),
_('option', {value: 'name'}, 'Sort by: Name'),
_('option', {value: 'price_low'}, 'Sort by: Price (low to high)'),
_('option', {value: 'price_high'}, 'Sort by: Price (high to low)'),
])
])
]),
new PlacedLive(this.data, (r) => {
if(r === null) return _('div', {className: 'loading'}, 'Loading...');
if(r.total === 0) return _('div', 'No resources found');
return _('div', {className: 'resources', children: r.resources.map((e) => {
return new Resource(e);
})});
})
]}
}
};
;
class MyOwnedResources extends Ophose.Component {
constructor(props) {
super(props);
}
render() {
return {_: 'div', children: [
_('h1', 'Owned Resources'),
new ResourceList({fetchPoint: '/resource/bought'})
]}
}
};
;
class MyResources extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self .upload {
display: flex;
justify-content: flex-end;
padding: 1em 0;
}
`
}
render() {
return {_: 'div', children: [
_('div', {className: 'upload'},
new Link({href: '/resource/new', className: 'button', c: 'Upload a resource'})
),
_('h1', 'Your resources'),
new ResourceList({fetchPoint: '/resource/my'})
]}
}
};
;
class MyWatchlist extends Ophose.Component {
constructor(props) {
super(props);
}
render() {
return {_: 'div', children: [
_('h1', 'Watchlist'),
new ResourceList({fetchPoint: '/resource/watchlist'})
]}
}
};
class MyPayout extends Ophose.Component {
constructor(props) {
super(props);
this.data = new Live(null);
this.message = new Live(null);
oenv('ah4/user/profile')
.then((data) => {
this.data.set(data);
});
}
style() {
return /* css */`
%self {
display: flex;
flex-direction: column;
gap: 5rem;
align-items: center;
text-align: center;
}
%self p {
color: var(--text-color-light);
}
%self .payout-info {
display: flex;
flex-direction: column;
gap: 1rem;
}
%self .payout-info h1 {
font-size: 4rem;
font-weight: 600;
}
%self .payout-info .error {
color: red;
}
%self .payout-info .success {
color: var(--text-color-light);
}
`
}
payout(e) {
e.target.disabled = true;
oenv('resource/payout')
.then((data) => {
this.data.set({
...this.data.get(),
balance: 0
});
this.message.set(data);
})
.catch((data) => {
e.target.disabled = false;
this.message.set(data.responseJSON);
});
}
render() {
return new PlacedLive(this.data, this.message, (data, message) => {
if(data === null) return _('p', {className: 'loading'}, 'Loading...');
return {_: 'div', className: 'payout', children: [
_('h2', 'Payout'),
_('div', {className: 'payout-info'},
_('p', 'You can withdraw your earnings here. You actually have:'),
_('h1', '$' + data.balance.toFixed(2)),
message && message.error && _('p', {className: 'error'}, message.error),
message && message.message && _('p', {className: 'success'}, message.success)
),
_('button', {className: 'button', onclick: (e) => {this.payout(e)}}, 'Withdraw'),
_('p', 'Note that you can only withdraw your earnings once you have reached $5.00.')
]}
});
}
};
;
class ResourceEdit extends Ophose.Component {
constructor(props) {
super(props);
this.isNew = this.props.isNew ?? true;
this.dataEndpoint = (this.props.resourceId && '/resource/get/' + this.props.resourceId) ?? undefined;
this.versions = new Live(null);
if(!this.isNew) {
oenv('resource/versions/' + this.props.resourceId)
.then(r => {
this.versions.set(r.versions);
})
}
}
style() {
return /* css */`
%self {
padding: 2rem 0;
}
%self .table {
margin: 2rem 0;
}
`
}
onResourceLoaded(data) {
if(!data.own) {
route.go('/resource/' + data.id);
}
}
render() {
return {_: 'div', children: [
new XForm({
className: 'form',
dataEndpoint: this.dataEndpoint,
onDataLoaded: this.onResourceLoaded,
endpoint: this.isNew ? 'resource/add' : 'resource/update/' + this.props.resourceId,
onSuccess: (data) => {
route.go('/resource/' + data.id);
},
c: [
!this.isNew && new Link({c: '< Back', href: '/resource/' + this.props.resourceId}),
!this.isNew && _('input', {type: 'hidden', name: 'id', value: this.props.resourceId}),
_('div', {className: 'row_group'}, [
new XInput({name: 'name', style: 'flex: 1;', label: 'Resource name', required: this.isNew ? true : undefined, disabled: this.isNew ? undefined : true, maxlength: 32}),
new XInput({name: 'price', style: 'flex: 1;', label: 'Price (in $)', type: 'number', step: 0.01, value: 0.00}),
]), // row group
new XInput({name: 'description', label: 'Description', type: 'textarea', required: true, maxlength: 2048}),
new XInput({name: 'image', label: 'Image', type: 'file', required: this.isNew ? true : undefined}),
new XInput({name: 'tags', label: 'Tags (split by ,)', maxlength: 256}),
_('div', {className: 'row_group'}, [
new XInput({name: 'version_name', style: 'flex: 1;', label: 'Version name', required: this.isNew ? true : undefined}),
this.isNew && new XInput({name: 'resourceType', style: 'flex: 1;', label: 'Resource type', type: 'select', required: true, options: [
{value: 1, text: 'Component'},
{value: 2, text: 'Environment'},
]}),
]),
new XInput({name: 'version_description', label: 'Version description', type: 'textarea', required: this.isNew ? true : undefined}),
new XInput({name: 'version', label: 'Version file', type: 'file', required: this.isNew ? true : undefined}),
new XInput({name: 'documentation', label: 'Documentation', type: 'file'}),
_('button', {type: 'submit'}, 'Save'),
] // XForm children
}), // XForm
!this.isNew && new PlacedLive(this.versions, (versions) => {
return _('table', {className: 'table'}, [
_('tr', [
_('th', 'Version'),
_('th', 'Actions'),
]),
!versions
?
_('tr', [
_('td', {colspan: 2}, 'Loading...')
])
:
versions.map(version => {
return _('tr', [
_('td', version.version_name),
_('td', {style: 'display: flex; gap: 1rem;'}, [
_('button', {className: 'button', onclick: () => {
window.open(version.fileUrl, '_blank');
}}, 'Download'),
_('button', {className: 'button', onclick: () => {
oenv('resource/delete_version/' + this.props.resourceId + '/' + version.version_name)
.then(() => {
versions.splice(versions.indexOf(version), 1);
this.versions.set(versions);
})
}}, 'Delete'),
])
])
})
]) // table
}), // placed live
]}
}
}class Markdown extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
border-radius: 0.75rem;
padding: 1rem;
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
}
%self * {
white-space: pre-wrap;
word-wrap: break-word;
}
%self p {
color: var(--text-color-light);
}
%self pre {
background-color: var(--bg-color-light);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
}
%self code {
color: var(--text-color-light);
background-color: var(--bg-color-light);
padding: 0.25rem;
border-radius: 0.25rem;
}
%self ul {
padding-left: 1rem;
}
%self ul li {
color: var(--text-color-light);
margin: 0.5rem 0;
}
`
}
render() {
let html = md.render(this.props.text);
return {_: 'div', innerHTML: html}
}
};
class ResourceDoc extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
display: flex;
flex-direction: column;
gap: 1rem;
}
%self .controls {
display: flex;
flex-direction: row;
gap: 1rem;
justify-content: space-between;
align-items: center;
}
%self .controls h3 {
font-size: 1rem;
font-weight: 600;
}
%self .doc {
padding: 1rem;
border-radius: 0.75rem;
border: var(--comp-border);
margin-bottom: 1rem;
width: 100%;
}
`
}
render() {
return {_: 'div', children: [
new Markdown({text: this.props.documentation ?? 'No documentation available.'})
]}
}
}class PaymentButton extends Ophose.Component {
constructor(props) {
super(props);
this.paymentEndpoint = props.paymentEndpoint;
}
style() {
return /* css */`
%self {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
`
}
createLoading() {
let blocker = document.createElement('div');
blocker.style.position = 'fixed';
blocker.style.top = '0';
blocker.style.left = '0';
blocker.style.width = '100%';
blocker.style.height = '100%';
blocker.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
blocker.style.zIndex = '1000';
document.body.appendChild(blocker);
let loading = document.createElement('div');
loading.style.position = 'fixed';
loading.style.top = '50%';
loading.style.left = '50%';
loading.style.transform = 'translate(-50%, -50%)';
loading.style.width = '100px';
loading.style.height = '100px';
loading.style.borderRadius = '50%';
loading.style.border = '5px solid #f3f3f3';
loading.style.borderTop = '5px solid #3498db';
loading.style.animation = 'spin 2s linear infinite';
blocker.appendChild(loading);
return blocker;
}
pay() {
if(!this.paymentEndpoint) {
throw new Error('Payment endpoint is not defined.');
}
let blocker = this.createLoading();
oenv(this.paymentEndpoint)
.then((res) => {
if(this.props.beforePay && !(this.props.beforePay(res))) {
document.body.removeChild(blocker);
return;
};
let paymentUrl = res.paymentUrl;
if(!paymentUrl) {
document.body.removeChild(blocker);
this.onPaymentComplete();
console.log('Payment URL is not defined.');
return;
}
let paymentWindow = window.open(paymentUrl, "popup", "width=600,height=600");
let paymentInterval = setInterval(() => {
if(paymentWindow.closed) {
clearInterval(paymentInterval);
document.body.removeChild(blocker);
this.onPaymentComplete();
}
}, 1000);
})
.catch((data) => {
this.props.beforePay && this.props.beforePay(data);
document.body.removeChild(blocker);
});
}
onPaymentComplete() {
if(this.props.onCompleteUrl) {
route.go(this.props.onCompleteUrl);
return;
}
window.location.reload();
}
render() {
return {_: 'button', className: 'button', onclick: () => this.pay(), children: [
{_: 'span', className: 'bi bi-credit-card'},
{_: 'span', className: 'bi bi-paypal'},
this.props.children
]}
}
}class Tutorial extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
letter-spacing: 0.05rem;
display: flex;
position: relative;
margin: 0 0 5rem;
}
%self .chapters {
flex: 0;
padding: 2rem;
position: fixed;
right: 0;
z-index: 300;
height: 100%;
width: 20%;
top: 0;
display: ${PageTutorial.LIVES.chapters_visible == true ? "block" : "none"};
background-color: var(--bg-color);
}
%self .chapters a {
display: block;
font-size: 0.825rem;
margin: 1rem 0;
cursor: pointer;
text-decoration: none;
text-transform: uppercase;
transition: all 0.25s;
color: var(--text-color-light);
}
%self .chapters a:hover {
color: var(--text-color);
}
%self .t_content {
flex: 0.8;
max-width: 80%;
padding: 1rem;
position: relative;
}
%self .t_content li sup {
padding: 0.25rem 0.5rem;
}
%self .title {
font-size: 3rem;
font-weight: 800;
margin-bottom: 0.5rem;
}
%self .descripion {
font-size: 1.25rem;
font-weight: 400;
margin-bottom: 1rem;
}
%self h2 {
font-size: 1.75rem;
font-weight: 800;
margin: 8rem 0 1.5rem;
}
%self ul li :not(b) {
color: var(--text-color-light);
}
%self p {
font-size: 1rem;
font-weight: 500;
margin: 0.5rem 0;
font-family: var(--font-sen);
line-height: 2rem;
}
%self pre {
font-size: 0.9rem;
margin: 3rem 0;
}
%self h3 {
font-size: 1.5rem;
font-weight: 800;
margin: 3rem 0 1rem;
text-align: center;
}
%self .t_content b {
background-color: var(--comp-bg-color);
padding: 0.25rem 0.5rem;
font-weight: 600;
border-radius: 0.5rem;
margin: 0.25rem 0;
display: inline-block;
}
%self .t_content img {
width: 100%;
border-radius: 0.5rem;
margin: 1rem 0;
}
%self .rendered {
margin: 1rem 0;
width: 100%;
border: var(--comp-border);
border-radius: 0.5rem;
overflow: hidden;
padding: 1rem;
transition: all 0.25s;
position: relative;
}
.rendered:hover {
padding: 1.125rem 1rem;
}
`
}
styles() {
return {
'md': /* css */`
%self .chapters {
display: none;
}
%self .t_content {
max-width: 100%;
}
`
}
}
getTitle() {
return this.title;
}
getDescription() {
return this.description;
}
onPlace(element) {
let chapters = element.getElementsByClassName('chapters')[0];
let content = element.getElementsByClassName('t_content')[0];
content.querySelectorAll('h2').forEach((h2, i) => {
let id = `chapter-${i}`;
h2.id = id;
let a = document.createElement('a');
a.innerText = h2.innerText;
a.onclick = () => {
let h = document.getElementById(id);
if (h) {
h.scrollIntoView({
behavior: 'smooth'
});
}
}
chapters.appendChild(a);
});
}
setTitle(title) {
this.title = title;
}
setDescription(description) {
this.description = description;
}
setContent(content) {
this.content = content;
}
render() {
return {_: 'div', children: [
_('div', {className: 't_content'}, [
_('h1', { className: 'title' }, this.title),
_('p', { className: 'descripion' }, this.description),
...this.content
]),
_('div', { className: 'chapters' }, [
_('h4', 'Chapters')
])
]}
}
};
;
;
class TutorialGetStarted extends Tutorial {
constructor() {
super({});
this.liveMsg = new Live("No message yet...");
let getRdmMsg = () => {
oenv("tutorial/random_message")
.then(response => this.liveMsg.set(response.message));
};
this.setTitle('Get started');
this.setDescription('You are new to Ophose? This tutorial will help you get started in less than 5 minutes! You will learn how to create a new project, run it and deploy it and key concepts behind this framework logic without going in details (see further parts to).')
this.setContent([
_('h2', 'Create and run your first project'),
_('p', [
'First, you need to create a new project. You can do this by ',
new Link({href: 'https://github.com/ah-4/ophose-release', c: _('u', 'donwloading Ophose')}),
' and put the files into your Apache server root folder. Make sure you have ',
_('a', {href: 'https://www.php.net/manual/en/install.php', target: '_blank'}, ['PHP 8 or later', _('sup', {className: 'bi bi-box-arrow-up-right'})]),
' installed and enabled on your server with following extensions:',
_('ul', [
_('li', 'pdo'),
_('li', 'pdo_mysql'),
_('li', 'curl'),
_('li', 'zip')
]),
_('br'),
'Then, here you go! Launch your server and you may already be able to access your Ophose web application by going to ',
_('a', {href: 'http://localhost', target: '_blank'}, ['http://localhost (if default-configurated)', _('sup', {className: 'bi bi-box-arrow-up-right'})]),
'. This will be start of your application. We can now dive in main files and folders of your project you will need to know to get started:',
_('ul', [
_('li', [
_('b', '/project.oconf'),
': this is the main configuration of your project. You can change the name of your project, the production mode and more...'
]),
_('li', [
_('b', '/api/'),
': you can create your API endpoints, there but we will see this in a later tutorial.'
]),
_('li', [
_('b', '/components/'),
': this is where you will create your components. Components are the main part of your front user interface. You can create as many components as you want, use them into other components and make them communicate between each other.'
]),
_('li', [
_('b', '/env/'),
': this is where you will put your environments. They are the main logic of your application. You can create as many environments as you want, use them into other environments and make them communicate between each other and with the front-end. They are protected by default against XSS and CSRF attacks.'
]),
_('li', [
_('b', '/modules/'),
': Modules are in this folder. They are used in your user interface whenever you need to use some functionalities independently from the components. For example, you can create a module to send a form data as JSON to your environments and way more...'
]),
_('li', [
_('b', '/ophose/ (DO NOT TOUCH)'),
': this is the main folder of Ophose. You should not touch it... Please do not touch it... Please...'
]),
_('li', [
_('b', '/pages/'),
': this is where you will create your pages. Pages are in JavaScript and are used to create your routes. They can understand dynamic routes such as ',
_('i', '/user/:username'),
' for example and more...'
]),
_('li', [
_('b', '/public/'),
': this is where you will put your public files such as images, CSS and JavaScript files that can be directly accessed...'
]),
])
]),
_('h2', 'Your first component'),
_('p', [
'Now that you know where to put your files, let\'s create your first component.',
_('br'),
_('br'),
'Firstly, what is a component ? A component is a part of your user interface. It can be a button, a text or whatever... It is important to see and use them as independent reusable parts of your user interface.',
_('br'),
_('br'),
'For example, you can create a button component and use it in your header and footer components themselves. A component is a JavaScript class that extends ',
_('b', 'Ophose.Component'),
'. By default, the main methods you will need to override are ',
_('b', 'constructor(props)'),
', ',
_('b', 'style()'),
' and ',
_('b', 'render()'),
'.',
_('br'),
_('br'),
'Theory is good but let\'s create our first component to understand it better. Create a new file ',
_('b', '/components/MyComponent.js'),
' and put the following code in it:',
new CodeBlock({
lines: [
'class MyComponent extends Ophose.Component {',
' constructor(props) {',
' super(props);',
' }',
'',
' style() {',
' return /* css */`',
' %self {',
' color: red;',
' }',
' `',
' }',
'',
' render() {',
' return {_: "div", children: [',
' "Hello world! The title is ",',
' _("b", this.props.title)',
' ]}',
' }',
'}'
]
}),
_('h3', 'constructor(props)'),
'The constructor is the first method called when your component is created. It may take any argument you want but it is have to call the Ophose.Component constructor with the props argument. ',
_('br'),
_('br'),
_('b', 'props'),
' is an object that contains all the properties you will pass to your component. It does understand HTML attributes such as ',
_('i', 'id'),
', ',
_('i', 'className'),
' and more... You can also pass your own properties to your component. For example, you can pass a ',
_('i', 'title'),
' property to your component to display a title in it by simply accessing to it.',
_('h3', 'style()'),
'The ',
_('b', 'style()'),
' method is used to style your component. It is a method that returns a CSS string. You can use any CSS and if you want to only style your component, you can use ',
_('b', '%self'),
' to target it.',
_('p', {className: 'alert'}, 'Note: the style() method is not required. If you do not override it, your component will not be styled. Plus, it will only affect the component instance itself and its children.'),
_('h3', 'render()'),
'The ',
_('b', 'render()'),
' method is used to render your component. It must return an object that will be used to render your component. To describe your component you may either use an JavaScript object and use the properties you need such as',
_('ul', [
_('li', [
_('b', '_'),
': the tag name of your component. It can be ',
_('i', 'div'),
', ',
_('i', 'span'),
' or any other HTML tag name.'
]),
_('li', [
_('b', 'c'),
' or ',
_('b', 'children'),
': the children of your component. It can be a string, an object, an array of objects or an Ophose.Component.'
]),
_('li', [
_('b', 'id'),
': the id of your component. It is used to target your component with CSS or JavaScript.'
]),
_('li', [
_('b', 'className'),
': the class names of your component.'
])
]),
_('p', {className: 'alert'}, 'Note: the render() method is required. If you do not override it, your component will not be rendered. Plus, only the tag name is required.'),
]),
_('h2', 'How to use your component ?'),
_('p', [
'Now that you have created your component, you may want to use it. To do so, you need to call it in a component or a page. For example, you may have noticed the file ',
_('b', '/components/Base.js'),
'which by default is',
new CodeBlock({
lines: [
'class Base extends Ophose.Base {',
' constructor(props) {',
' super(props);',
' }',
'',
' render() {',
' return {_: "div", children: [',
' {_: "div", id: "page", children: this.props.children}',
' ]}',
' }',
'}'
]
}),
' that is the base component of your application. It is used to load and display the user interface that\'ll be static through all pages. You can use your component in it by simply calling it in the render() method:',
new CodeBlock({
lines: [
'class Base extends Ophose.Base {',
' constructor(props) {',
' super(props);',
' }',
'',
' render() {',
' return {_: "div", children: [',
' // Your component is here',
' new MyComponent({title: "My title"}),',
' {_: "div", id: "page", children: this.props.children}',
' ]}',
' }',
'}'
]
}),
'which will render your app as',
_('div', {className: 'rendered'}, [
'Hello world! The title is ',
_('b', {style: 'color: red;'}, 'My title')
])
]),
_('h2', 'Your first page'),
_('p', [
'Now that you know how to create a component, let\'s create your first page. A page is a JavaScript class that extends ',
_('b', 'Ophose.Page'),
'. By default, the main methods you will need to override are ',
_('b', 'constructor(props)'),
', ',
_('b', 'style()'),
' and ',
_('b', 'render()'),
' too.',
_('br'),
_('br'),
'But how to define a route for your page ? Ophose can automtically understand dynamic pages with its routing system. You can do so by creating the page file in yours ',
_('b', '/pages/'),
' folder and by associating with the route you want... For example: ',
_('ul', [
_('li', [
_('b', '/pages/index.js'),
': the index page of your application. It is the default page that will be loaded when you go to ',
_('i', 'http://localhost/'),
]),
_('li', [
_('b', '/pages/error.js'),
': the error page'
]),
_('li', [
_('b', '/pages/auth/login.js'),
': you could imagine a login page in your application at ',
_('i', 'http://localhost/auth/login'),
]),
_('li', [
_('b', '/pages/user/_username/index.js'),
': you could also imagine a user page in your application at ',
_('i', 'http://localhost/user/ah4'),
_('p', {className: 'alert'}, 'Note that you have to create a folder starting with an underscore to create a dynamic route then the index.js file it self. Why not only a dynamic file ? Because you may want to create sub routes from the dynamic route (for example: http://localhost/user/ah4/followers)')
]),
_('br'),
_('br'),
'But at the moment, let\'s focus on creating your first page at ',
_('b', '/pages/test.js'),
'.',
new CodeBlock({
lines: [
'class PageTest extends Ophose.Page {',
' constructor(urlQueries) {',
' super(urlQueries);',
' }',
'',
' render() {',
' return {_: "section", children: [',
' "Welcome on the test page...",',
' ]}',
' }',
'}',
'',
'// Exporting your page',
'oshare(PageTest);'
]
})
]),
'Now, you can access your page at ',
_('i', 'http://localhost/test'),
' and you will see the following:',
_('div', {className: 'rendered'}, [
_('div', [
'Hello world! The title is ',
_('b', {style: 'color: red;'}, 'My title'),
]),
'Welcome on the test page...'
]),
'Because your component is rendered in your base component and your page is rendered in your base component too. Let\'s further explain this:',
_('h3', 'constructor(urlQueries)'),
'The constructor is the first method called when your page is created. It cannot take any argument you want because it has to call the Ophose.Page constructor with the urlQueries argument.',
_('br'),
_('br'),
_('b', 'urlQueries'),
' is an object that contains all the queries you will pass to your page. It does understand dynamic routes such as ',
_('i', '/user/ah4'),
' which will be ',
new CodeBlock({
lines: [
'urlQueries = {',
' username: "ah4"',
'}'
]
}),
_('h3', 'render()'),
'The ',
_('b', 'render()'),
' method is used to render your page. It must return an also object that will be used to render your page or a component.',
]),
_('h2', 'Create a bridge between your front-end and your back-end with environments'),
_('p', [
'An ',
_('b', 'Environment'),
' can be seen as a logic part of your web application. For example, let\'s say you want to create an forum web application. You\'ll need to store posts from users, comments, likes and more... You can create an environment for each of these parts. For example, you can create a ',
_('b', 'User environment'),
' to store users, but this environment will not be able to store posts, comments and likes. You will need a ',
_('b', 'Database environment'),
' to store all these data. So User will communicate with Database. But how to make them communicate between each other ?I\'ll try to explain this in this part as simple as possible.',
_('br'),
_('br'),
'Firstly, let\'s create a new environment called "MyEnvironment" at ',
_('b', '/env/MyEnvironment/'),
', create the file "env.php" inside and put the code in it:',
new CodeBlock({
lines: [
'endpoint("random_message", function() {',
' $messages = [',
' "Hello world!",',
' "Hello Ophose!",',
' "Hello everyone!",',
' "Hello there!"',
' ];',
'',
' return $messages[array_rand($messages)];',
' });',
' }',
'}'
],
language: 'php'
}),
_('p', {className: 'alert', style: 'display: block;'}, ['Note that either ', _('i', 'env.php'), ' or ', _('i', 'env.oconf'), ' is mandatory but can be empty (otherwise your environment won\'t be considered as one properly).']),
'You may have noticed that you do not need to import anything in your environment. This is because Ophose automatically imports the environment in your endpoints. Also the default response format is JSON.',
_('p', {className: 'alert'}, 'Note: for security reasons, you cannot access your environment directly. You have to create an endpoint to access it.'),
_('h3', 'How to call your environment ?'),
'Now that you have created your environment and your endpoint, you may want to call it. To do so, you can use the async Javascript function ',
_('b', 'oenv(endpoint, postData = {})'),
' which returns a Promise. For example, you can call your environment in your component by putting the following code in it (',
_('b', 'Live'),
' are explained in Components tutorial):',
new CodeBlock({
lines: [
'// In a component class',
'constructor(props) {',
' super(props);',
' this.message = new Live("No message yet...");',
'}',
'',
'displayRandomMessage() {',
' oenv("MyEnvironment/random_message")',
' .then(response => this.message.set(response.message));',
'}',
'',
'render() {',
' return {_: "div", children: [',
' _("button", {',
' onclick: this.displayRandomMessage',
' }, "Click me to show the message!"),',
' {_: "p", [',
' "The message is ",',
' this.message',
' }',
' ]}',
'}',
]
}),
'which will render your component as',
_('div', {className: 'rendered'}, [
_('button', {className: 'button', onclick: getRdmMsg}, 'Click me to show the message!'),
_('p', [
'The message is ',
_('span', {id: 'message'}, this.liveMsg)
])
]),
_('br'),
_('br'),
'To conclude, Ophose allows you to create complex and full web applications with a simple and easy to understand logic. You can create as many components, pages and environments as you want and make them communicate between each other. You can also create modules to make your components communicate with your environments and way more... Make sure to dive in the documentation, tutorials and project examples to learn more about this framework and how to use it properly.',
_('br'),
_('br'),
'Thank you for reading this tutorial and have a nice day! - AH4 <3'
])
]);
}
};
;
;
class TCList extends Ophose.Component {
constructor(props) {
super(props);
this.input = new Live("");
this.todos = new Live([
"Do the dishes",
"Clean the house",
"Buy milk"
]);
}
style() {
return /* css */`
%self {
}
`
}
render() {
return {_: 'div', children: [
{_:'div', className: 'row', style: 'gap: 1rem;', children: [
{_:'input', className: 'input', placeholder: 'What\'s next to do?', type: 'text', oninput: (e) => this.input.set(e.target.value)},
{_:'button', className: 'button', text: 'Add', onclick: () => {
this.todos.set([...this.todos.value, this.input.value]);
}},
]},
new PlacedLive(this.todos, (todos) => {
return {_:'ul', children: [
todos.map((todo, index) => {
return {_:'li', children: [
todo,
{_:'button', className: 'button', style: 'margin-left: 1rem; font-size: 0.75em;', text: 'Delete', onclick: () => {
this.todos.value.splice(index, 1);
this.todos.set(this.todos.value);
}},
]};
})
]};
}),
]}
}
}
class TutorialComponents extends Tutorial {
constructor() {
super({});
this.count = new Live(0);
this.countSharedButtons = new Live(0);
let placeSharedButton = () => {
return _('button', {className: 'button', onclick: () => {
this.countSharedButtons.set(this.countSharedButtons + 1);
}}, ['You clicked: ', this.countSharedButtons, ' times'])
};
let placeButtonCount = () => {
return _('button', {className: 'button', onclick: () => {
this.count.set(this.count + 1);
}}, ['You clicked: ', this.count, ' times']);
};
this.setTitle('Components & Live');
this.setDescription('Dive into Ophose components and its concepts to create your dream User Interface. You will see how to create a component, how to use it and how to make it communicate with other components and make them dynamic with Live concepts.')
this.setContent([
_('p', [
_('h2', 'What is a component ?'),
'In simple words, a component is a front-end part is of your web application. It can be a button, a text, a form, a menu... Whatever you want.',
_('br'),
_('br'),
'You can see your user interface as a tree of components. For example, a button can be a component, and a form can be a component that contains a button component. In plus, they can communicate with each other.',
_('br'),
_('br'),
'Ophose components are classes that extends the ',
_('code', 'Ophose.Component'),
' class. In this tutorial, we will see only basics of components with you can mostly do everything you want. If you want to go further, you can read the documentation about it.',
_('br'),
_('br'),
'To create a component, create your own class extending ',
_('code', 'Ophose.Component'),
' and implement these 3 methods:',
new CodeBlock({
lines: [
'class MyComponent extends Ophose.Component {',
' constructor(props) {',
' super(props);',
' }',
'',
' style() {',
' return /* css */`',
' %self { }',
' `;',
' }',
'',
' render() {',
' return {',
' _: "div",',
' children: "Hello World! This is: " + this.props.name',
' };',
' }',
'}',
]
}),
_('ul', [
_('li', [
_('b', 'constructor(props)'),
_('sup', 'constructor'),
': the constructor of your component. It takes a ',
_('b', 'props'),
' parameter which is an object containing all the properties of your component. You can access to it with ',
_('b', 'this.props'),
'. Note that by default, the props is an empty object if not specified.'
]),
_('li', [
_('b', 'style()'),
_('sup', 'CSS style'),
': this method returns the style of your component. It is a string containing CSS code. You can use the ',
_('b', '%self'),
' selector to select your component. Note that by default, the style is an empty string if not specified.'
]),
_('li', [
_('b', 'render()'),
_('sup', 'Ophose object'),
': this method returns the HTML desciption of your component. It is an object containing the HTML description code. You can use the ',
_('b', '_'),
' key to specify the HTML tag of your component. You can use the ',
_('b', 'children'),
' key to specify the children of your component. Note that by default, the children is an empty object if not specified. Also, it automatically associate HTML attributes and events to your component. You can use the ',
_('b', 'this.props'),
' object to access to your component properties. Inside of its children, you may put other Ophose objects, strings, Ophose objects, Live...'
]),
]),
_('h2', 'How to use a component ?'),
'Now that you know how to create a component, you need to know how to use it. To use a component, you need to create an instance of it. For example, you can place it in your page like this:',
new CodeBlock({
lines: [
'class MyPage extends Ophose.Page {',
' constructor() {',
' super();',
' }',
'',
' render() {',
' return {_:"div", children: [',
' new MyComponent({name: "Night"}),',
' ]};',
' }',
'}',
]
}),
'Your page will now render: ',
_('div', {className: 'rendered'}, [
_('div', 'Hello World! This is: Night'),
]),
'But as said before, you can put HTML attributes and events to your component from the parent component. For example, you can do this:',
new CodeBlock({
lines: [
'class MyPage extends Ophose.Page {',
' constructor() {',
' super();',
' }',
'',
' render() {',
' return {_:"div", children: [',
' new MyComponent({name: "Night", style: "color: blue;", onclick: () => {',
' alert("Hello!");',
' }}),',
' ]};',
' }',
'}',
]
}),
'which will now render:',
_('div', {className: 'rendered'}, [
_('div', {style: 'color: blue;', onclick: () => {
alert('Hello!');
}}, 'Hello World! This is: Night'),
]),
_('h2', 'Rendering a list'),
'You can render a list of components by placing them in an array then placing this array in the children of your component. This array may only contains Ophose objects. But if the structure is more complex, you can use the ',
_('b', 'Array.map()'),
' function to transform your array of objects to an array of Ophose objects.',
_('br'),
_('br'),
'For example, you can create this component:',
new CodeBlock({
lines: [
'class MyList extends Ophose.Component {',
' constructor(props) {',
' super(props);',
' this.items = [',
' "Hello",',
' "World",',
' "!"',
' ];',
' }',
'',
' render() {',
' return {',
' _: "p",',
' children: [',
' "My list is:",',
' ...this.items',
' };',
' }',
'}',
]
}),
'which will render:',
_('div', {className: 'rendered'}, [
_('div', [
'My list is:',
_('p', 'Hello'),
_('p', 'World'),
_('p', '!'),
]),
]),
'Why ? Because the ',
_('b', 'items'),
' array contains only strings. So, Ophose will render them as text. But if you want to render them as components, you can do this:',
new CodeBlock({
lines: [
'class MyList extends Ophose.Component {',
' constructor(props) {',
' super(props);',
' this.items = [',
' "Hello",',
' "World",',
' "!"',
' ];',
' }',
'',
' render() {',
' return {',
' _: "ul",',
' children: this.items.map(item => {',
' return {_:"li", children: item};',
' })',
' };',
' }',
'}',
]
}),
'giving you:',
_('div', {className: 'rendered'}, [
_('ul', [
_('li', 'Hello'),
_('li', 'World'),
_('li', '!'),
]),
]),
_('h2', 'How to make a component dynamic ?'),
'A component is dynamic when it can change its content after it has been placed. For example, a button can change its text when clicked. To do this, I introduce you the Live concept. A Live is a variable that can be placed in your component and can be updated asynchronously and after it has been placed. To create a Live, you can do this:',
new CodeBlock({
lines: [
'class ButtonCount extends Ophose.Component {',
' constructor(props) {',
' super(props);',
' this.count = new Live(0);',
' }',
'',
' render() {',
' return {',
' _: "button",',
' children: ["You clicked: ", this.live, " times"],',
' onclick: () => {',
' this.count.set(this.count + 1);',
' }',
' };',
' }',
'}',
]
}),
_('p', {className: 'alert'}, "Note that to display it must be placed in your component as a child."),
'We created a Live named ',
_('b', 'count'),
' which is a number and has a default value of ',
_('b', '0'),
'. And when we click on the button, we increment the value of the Live by one by calling the ',
_('b', 'set(newValue)'),
' method of the Live. If you place a Live in your ',
_('b', 'style()'),
' method, it will automatically update the style of your component.',
_('br'),
_('br'),
'Now, we can place it in our page like this:',
new CodeBlock({
lines: [
'class MyPage extends Ophose.Page {',
' constructor() {',
' super();',
' }',
'',
' render() {',
' return {_:"div", children: [',
' new ButtonCount()',
' ]};',
' }',
'}',
]
}),
'which will now render:',
_('div', {className: 'rendered'}, [
placeButtonCount()
]),
_('h3', 'Using props to pass any object ?'),
'You can pass any object to your component by using the ',
_('b', 'props'),
' parameter of the constructor. Meaning that you can pass a Live to your component and re use it in your component. Let\'s improve our button by passing a Live to it:',
new CodeBlock({
lines: [
'class ButtonCount extends Ophose.Component {',
' constructor(props) {',
' super(props);',
' }',
'',
' render() {',
' return {',
' _: "button",',
' children: ["You clicked: ", this.props.count, " times"],',
' onclick: () => {',
' this.props.count.set(this.props.count + 1);',
' }',
' };',
' }',
'}',
]
}),
'Now, we can place it as much as we want in our page and they will all share the same Live:',
new CodeBlock({
lines: [
'class MyPage extends Ophose.Page {',
' constructor() {',
' super();',
' this.count = new Live(0);',
' }',
'',
' render() {',
' return {_:"div", children: [',
' new ButtonCount({count: this.count}),',
' new ButtonCount({count: this.count}),',
' new ButtonCount({count: this.count}),',
' ]};',
' }',
'}',
]
}),
'which will now render:',
_('div', {className: 'rendered', style: 'display: flex; justify-content: space-between;'}, [
placeSharedButton(),
placeSharedButton(),
placeSharedButton(),
]),
_('h3', 'Render more complex dynamic values ?'),
'You can render more complex values by using the ',
_('b', 'PlacedLive'),
' concept. A PlacedLive is a Live that has a callback function to transform the placed HTML description by another asynchronously.',
_('p', {className: 'alert'}, "Note that the callback function must return a single Ophose object."),
'For example, you can do this:',
new CodeBlock({
lines: [
'class TodoList extends Ophose.Component {',
' constructor(props) {',
' super(props);',
'',
' this.input = new Live("");',
' this.todos = new Live([',
' "Do the dishes",',
' "Clean the house",',
' "Buy milk"',
' ]);',
' }',
'',
' render() {',
' return {',
' _: "div", children: [',
' {_:"input", type: "text", oninput: (e) => this.input.set(e.target.value)},',
' {_:"button", text: "Add", onclick: () => {',
' this.todos.set([...this.todos.value, this.input.value]);',
' }},',
' new PlacedLive(this.todos, (todos) => {',
' return {_:"ul", children: [',
' todos.map((todo, index) => {',
' return {_:"li", children: [',
' todo,',
' {_:"button", text: "Delete", onclick: () => {',
' this.todos.value.splice(index, 1);',
' this.todos.set(this.todos.value);',
' }},',
' ]};',
' })',
' ]};',
' }),',
' ]',
' };',
' }',
'}',
]
}),
'rendering:',
_('div', {className: 'rendered'}, [
new TCList()
]),
_('h2', 'Render children in your component'),
'You can add children to your component by using the ',
_('b', 'props.children'),
' key in the ',
_('b', 'render()'),
' method of your component. For example, you can do this:',
new CodeBlock({
lines: [
'class ParentComponent extends Ophose.Component {',
' constructor(props) {',
' super(props);',
' }',
'',
' render() {',
' return {',
' _: "div",',
' children: [',
' "Hello World! My children are ",',
' this.props.children',
' ]',
' };',
' }',
'}',
]
}),
_('p', {className: 'alert'}, "If an element of children is a list, it will be flatten and its elements will be added to the children of the component!"),
'Now, we can place it in our page like this:',
new CodeBlock({
lines: [
'class MyPage extends Ophose.Page {',
' constructor() {',
' super();',
' }',
'',
' render() {',
' return {_:"div", children: [',
' new ParentComponent({children: [',
' "a text and ",',
' new ButtonCount()',
' ]}),',
' ]};',
' }',
'}',
]
}),
'which display you:',
_('div', {className: 'rendered'}, [
_('div', ['Hello World! My children are a text and ', placeButtonCount()]),
]),
_('h2', 'Importing components'),
'You can import components from other files by using the ',
_('b', 'oimpc(path)'),
' function. As parameter, you need to specify the path of the component from components folder you want to import. For example, if you want to import a component at ',
_('b', '/components/test/MyComponent.js'),
', you can do this:',
new CodeBlock({
lines: [
'oimpc("test/MyComponent");',
]
}),
_('p', {className: 'alert'}, "Note that you don't need to specify the .js extension and the path root is /components/."),
_('h2', 'Components lifecycle'),
'Components have a lifecycle. This means that you can execute code when the component is created, destroyed... In order, the lifecycle methods are:',
_('ul', [
_('li', [
_('b', 'onPlace(element)'), ' is called when the component is placed in the DOM. The ', _('b', 'element'), ' parameter is the HTML node of the component.'
]),
_('li', [
_('b', 'onRemove()'), ' is called before the component is about to be removed.'
]),
]),
_('h2', 'Conclusion'),
'You now know how to create a component, how to use it and how to make it dynamic with Live concepts. You can now go to the next tutorial to learn how to create a page and how to use it.'
]),
]);
}
};
;
;
class TutorialBase extends Tutorial {
constructor() {
super({});
this.setTitle('Base');
this.setDescription('When your web application is loaded, the first thing that is loaded is the base. The base is the root component of your web application.');
this.setContent([
_('h2', 'What is the base ?'),
_('p', [
'The ', _('b', 'Base'), ' is the root component of your web application. It is the first component that is loaded when your web application is loaded. And why using a base ? Because it will allow you to have a common layout for all your pages without reloading it every time you go to a new page. To make it work, you must create a file called ', _('b', 'Base.js'), ' in the ', _('b', '/components/'), ' folder. Then, you can create your base like this:',
new CodeBlock({
lines: [
'class Base extends Ophose.Base {',
' // Note that the name of the class must be',
' // "Base" and it must extends "Ophose.Base"',
' constructor(props) {',
' super(props);',
' }',
'',
' render() {',
' return _("div",',
' new Header(),',
' this.props.children,',
' new Footer()',
' );',
' }',
'}',
]
}),
'Then every time you will go to a new page, the base will be loaded and the content of the page will be loaded in the ', _('b', 'main'), ' tag.',
_('p', {className: 'alert', style: 'display: block;'}, ['Note that you need to place the ', _('b', 'this.props.children'), ' in the ', _('b', 'children'), '. Otherwise, the content of the page will not be loaded.']),
]),
_('h2', 'Does it work as a normal component ?'),
_('p', [
'Yes, the base works as a normal component as it extends ', _('b', 'Ophose.Component'),'. You can add styles, events, etc.',
]),
]);
}
};
;
;
class TutorialPages extends Tutorial {
constructor() {
super({});
this.setTitle('Pages');
this.setDescription('Pages are also an important part of Ophose concepts. They are used to create a full and dynamic website. Basically, you will see how to easily create routes, how to create a page and how to use it.');
this.setContent([
_('h2', 'How to create a page ?'),
_('p', [
'A page is simply an Ophose component but to specify its route, you must put it in the right location. You may have noticed that 2 pages are created by default:', _('ul', [
_('li', [
_('b', '/pages/index.js'), ' which is the default page (the one that is displayed when you go to the root of the website).'
]),
_('li', [
_('b', '/pages/error.js'), ' which is the page displayed whenever there is an error (page not found...).'
]),
]),
'To create a new page, you must create a new file in the ', _('b', '/pages'), ' folder. For example, let\'s create a page called ', _('b', 'about.js'), ' in the ', _('b', '/pages'), ' folder. Then, you can create your page like this:',
new CodeBlock({
lines: [
'class PageAbout extends Ophose.Page {',
' constructor(urlQueries) {',
' super(urlQueries);',
' }',
'',
' render() {',
' return _("div", {className: "rendered"},',
' _("h1", "About"),',
' _("p", "This is the about page !")',
' );',
' }',
'}',
'',
'oshare(PageAbout);'
]
}),
'Then, you can go to ', _('b', [_('a', {href: 'http://localhost/about'}, 'http://localhost/about')]), ' and you will see your page !',
_('div', {className: 'rendered'}, [
_('h1', 'About'),
_('p', 'This is the about page !')
]),
'At the end of the file, you can see the line ', _('b', 'oshare(PageAbout);'), ' which is used to share the page. This is the same as ', _('b', 'export default PageAbout;'), ' but it also shares the page to Ophose. This is required to make the page work.',
_('br'),
_('br'),
'At the moment, don\'t worry about the ', _('b', 'urlQueries'), ' parameter. This is used to get the URL queries. For example, if you go to ', _('b', 'http://localhost/user/123'), ', the ', _('b', 'urlQueries.query'), ' parameter may be ', _('b', '{userId: 123}'), '. We will cover this later in this tutorial.',
]),
_('h2', 'How to create a dynamic page ?'),
_('p', [
'A dynamic page is a page that can change depending on the URL. For example, let\'s create a page that displays the user ID. To do this, we will use the ', _('b', 'urlQueries'), ' parameter. For example, if you go to ', _('b', 'http://localhost/user/123'), ', the ', _('b', 'urlQueries.query'), ' parameter may be ', _('b', '{userId: 123}'), '.',
'Let\'s create a new page called ', _('b', 'index.js'), ' in the ', _('b', '/pages/_userId/'), ' folder. Then, you can create your page like this:',
new CodeBlock({
lines: [
'class PageUser extends Ophose.Page {',
' constructor(urlQueries) {',
' super(urlQueries);',
' }',
'',
' render() {',
' return _("div",',
' _("h1", "User ID: " + this.urlQueries.query.userId),',
' _("p", "This is the user page !")',
' );',
' }',
'}',
'',
'oshare(PageUser);'
]
}),
'then you can go to ', _('b', [_('a', {href: 'http://localhost/user/123'}, 'http://localhost/user/123')]), ' for example and you\'ll see your page as expected:',
_('div', {className: 'rendered'}, [
_('h1', 'User ID: 123'),
_('p', 'This is the user page !')
]),
_('h3', 'Multiple URL queries ?'),
'You can also use multiple URL queries. For example, let\'s create a new page called ', _('b', 'index.js'), ' in the ', _('b', '/pages/user/_userId/_action/'), ' folder. Then, you can create your page like this:',
new CodeBlock({
lines: [
'class PageUserAction extends Ophose.Page {',
' constructor(urlQueries) {',
' super(urlQueries);',
' }',
'',
' render() {',
' return _("main",',
' _("h1", "User ID: " + this.urlQueries.query.userId + " - Action: " + this.urlQueries.query.action),',
' _("p", "This is the user page with an action !")',
' );',
' }',
'}',
'',
'oshare(PageUserAction);'
]
}),
'then you can go to ', _('b', [_('a', {href: 'http://localhost/user/123/delete'}, 'http://localhost/user/123/delete')]), ' for example and you\'ll see your page as expected:',
_('div', {className: 'rendered'}, [
_('h1', 'User ID: 123 - Action: delete'),
_('p', 'This is the user page with an action !')
]),
'You may also get your GET queries. For example, if you go to ', _('b', 'http://localhost/user/123/delete?confirm=true'), ', the ', _('b', 'urlQueries.get'), ' parameter may be ', _('b', '{confirm: "true"}'), '.',
]),
_('h2', 'Pages lifecycle'),
_('p', [
'Pages have a lifecycle. This means that you can execute code when the page is created, destroyed... In order, the lifecycle methods are:',
_('ul', [
_('li', [
_('b', 'onCreate()'), ' is called when the page is attempted to be created.'
]),
_('li', [
_('b', 'onLoad()'), ' is called when the page is loaded.'
]),
_('li', [
_('b', 'onPlace(element)'), ' is called when the page is placed. The ', _('b', 'element'), ' parameter is the HTML node of the page.'
]),
_('li', [
_('b', 'onLeave()'), ' is called when the page is about to be left. Generally when you go to another page.'
]),
]),
'For example, you can use these methods to load data from an API when the page is displayed and to destroy the data when the page is hidden.',
_('h3', 'Redirect to another page while loading ?'),
'You can also redirect to another page while loading. For example, if you want to redirect to the error page if the user is not logged in, you can override the ', _('b', 'onCreate()'), ' method like this:',
new CodeBlock({
lines: [
'onCreate() {',
' if(!userIsLoggedIn()) {',
' this.redirect("/error");',
' }',
'}'
]
}),
'Then, if the user is not logged in, the page will be redirected to the error page.',
_('p', {className: 'alert', style: 'display: block;'}, ['Note: you must redirect to another page in the ', _('b', 'onCreate()'), ' method. Otherwise, the page will be created and then redirected which could cause problems.']),
]),
_('h2', 'Conclusion'),
_('p', [
'We learned how to create a page, how to create a dynamic page and how to use the page lifecycle. This had covered only the basics of pages. If you want to learn more about pages, you can read the ', _('b', new Link({href: '/docs/pages', c: 'pages documentation'})), ' to learn more about pages.'
]),
]);
}
};
;
;
class TutorialEnvironments extends Tutorial {
constructor() {
super({});
this.setTitle('Environments & API');
this.setDescription('What would be a web application without any back-end nor logic ? In this tutorial, you will see Environments which are used to create the logic of your web application.');
this.setContent([
_('h2', 'Why is it called "Environment" ?'),
_('p', [
'The reason why it is called "Environment" is because you can see it as a part of your web application which will communicate with the back-end and the front-end. Basically, you can see it as an API. Let\'s take an example: you want to create a forum. You will need to use a ', _('b', 'Database'), ' environment to store the data of your forum (users, posts, etc.). Then, you will need to use a ', _('b', 'User'), ' environment communicating with the Database to create your users and manage sessions. And finally, you will need to use a ', _('b', 'Post'), ' environment which will communicate both with User and Database.',
]),
_('h2', 'What\'s the logic behind ?'),
_('p', [
'Let\'s take the same example as before, but with a schema:',
_('img', {src: '/img/tutorial/env_fetch_post.jpg', alt: 'Schema of the logic behind the environments'}),
'First, the user will send a request to the ', _('b', 'Post'), ' environment. As you can see, the request is asynchronous. This means that the user will not wait for the response of the environment and can send several requests at the same time. By default, the request is protected against CSRF (Cross-Site Request Forgery) and XSS (Cross-Site Scripting).',
_('br'),
_('br'),
'The request may look like this:',
new CodeBlock({
lines: [
'oenv("post/get", {',
' postId: 123',
'}).then((post) => {',
' console.log(post);',
'});',
'',
'// Output:',
'// {',
'// id: 123,',
'// title: "My post",',
'// content: "This is my post !"',
'// }'
]
}),
'The ', _('b', 'oenv'), ' function is used to send a request to an environment. The first parameter is the path of the environment, the second parameter is the endpoint path and the third parameter is the data of the request. The ', _('b', 'then'), ' function is used to get the response of the request. The response is the data returned by the function of the environment. You can also use the ', _('b', 'catch'), ' function to get the error of the request.',
_('p', {className: 'alert', style: 'display: block;'}, ['Note that you can only communicate with the environments by fetching endpoints for security reasons.']),
_('br'),
_('br'),
'At the moment, concepts like ', _('b', 'Database'), ', ', _('b', 'Request'), '... are not explained further. You will see them in more details in the next tutorials.'
]),
_('h2', 'How to create an environment ?'),
_('p', [
'To create an environment, you either need to create a file in your environment ', _('b', 'env.oconf'), ' (the configuration file of the environment, see a next chapter) or create a file in your environment ', _('b', 'env.php'), ' (the logic file of the environment). ',
'Let\'s take our example back, the ', _('b', 'Post'), ' environment will then work with the ', _('b', 'Database'), ' to fetch its data. The request may look like this in file ', _('b', '/env/post/env.php'), ':',
new CodeBlock({
lines: [
'endpoint("get", function() {',
' // Fetch the post in $myPost',
' Response::json($myPost);',
' });',
' }',
'',
'};'
],
language: 'php'
}),
'Firstly, the ', _('b', 'endpoints'), ' method is called. This method is used to create the endpoints of the environment which is interesting because you might create Middlewares and conditional rendering. ',
'The ', _('b', 'endpoint($path, $callback, $csrf = false, $methods = [], $required = [])'), ' function is used to create an endpoint. The first parameter is the path of the endpoint and the second parameter is the function of the endpoint. The function of the endpoint is called when the user sends a request to the endpoint. The function of the endpoint can return a value which will be sent to the user. In this case, the value is a post. The ', _('b', 'Response::json'), ' function is used to send a JSON response to the user. The ', _('b', 'Response::json($data, 400)'), ' function can also be used to send a JSON error to the user.',
_('br'),
_('br'),
_('h3', 'Communicate with other environments'),
'As you can see, the ', _('b', 'Post'), ' environment needs to communicate with the ', _('b', 'Database'), ' environment to get the user who created the post, its content and creation data. To do so, you can simply use the ', _('b', 'use'), ' keyword to import the ', _('b', 'Database'), ' environment and use it in your environment! Let\'s improve our example:',
new CodeBlock({
lines: [
'endpoint("get", function() {',
' // Using the Database environment',
' $db = Database::getDatabase();',
'',
' $stmt = $db->prepare("SELECT id, title, content, author_name, published_at FROM posts WHERE id = :id");',
' $stmt->execute([\'id\' => Request::post("id")]);',
' $myPost = $stmt->fetch(PDO::FETCH_ASSOC);',
' Response::json($myPost);',
' });',
' }',
'',
'};'
],
language: 'php'
}),
'The ', _('b', 'Database::getDatabase()'), ' function is used to get the database of the ', _('b', 'Database'), ' environment. Then, you can use it as you would do in a normal PHP file. Now, you can use the ', _('b', 'Post'), ' environment in your front-end to fetch the post!'
]),
_('h2', 'Dynamic endpoints'),
_('p', [
'In the previous example, the endpoint was static. This means that the endpoint was always the same even though it can change its output depending on the request. Let\'s take another example: you want to display a ', _('b', 'Product'), ' information by GET method, meaning you wouldn\'t pass POST information. You would want your request looks like this:',
new CodeBlock({
lines: [
'oenv("product/get/123").then((product) => {',
' console.log(product);',
'});',
'',
'// Output:',
'// {',
'// id: 123,',
'// name: "My product",',
'// price: 10',
'// }'
]
}),
'As you can see, the endpoint is dynamic because the ID of the product is passed in the endpoint. To create a dynamic endpoint, you can use the ', _('b', 'endpoint($path, $callback, $csrf = false, $methods = [], $required = [])'), ' function with a dynamic path. Let\'s take our example back:',
new CodeBlock({
lines: [
'endpoint("get/_id", function($id) {',
' // Using the Database environment',
' $db = Database::getDatabase();',
'',
' $stmt = $db->prepare("SELECT id, name, price FROM products WHERE id = :id");',
' $stmt->execute([\'id\' => $id]);',
' $myProduct = $stmt->fetch(PDO::FETCH_ASSOC);',
' Response::json($myProduct);',
' });',
' }',
'',
'};'
],
language: 'php'
}),
'As you can see, the endpoint path is now ', _('b', 'get/_id'), '. The ', _('b', '_id'), ' is a dynamic parameter which will be passed to the function of the endpoint. The dynamic parameter is passed as a parameter of the function of the endpoint. In this case, the dynamic parameter is ', _('b', '$id'), '.',
_('p', {className: 'alert', style: 'display: block;'}, ['The orders of the dynamic parameters in the endpoint path must be the same as the order of the parameters in the function of the endpoint. Also, the dynamic parameters must be prefixed by an underscore. Finally, endpoints order matters.']),
]),
_('h2', 'How to fetch from an environment ?'),
_('p', [
'In the previous examples, you have seen how to fetch from an environment. However, you can also fetch by going to a specific URL. Let\'s take our example back and admitting you\'ll need to fetch it from an other website, you would need to go to this URL:',
new CodeBlock({
lines: [
'https://mywebsite.com/@/post/get/123',
]
}),
'As you can see, the URL is composed of the website URL, the environment path, the endpoint path and the dynamic parameter. The dynamic parameter is passed as a query parameter. In this case, the dynamic parameter is ', _('b', 'id'), '. You see how easy it is to fetch from an environment ?', _('i', 'Ophose'), ' does all the work for you!',
]),
_('h2', 'Environment configuration'),
_('p', [
'The configuration of the environment is stored in the ', _('b', 'env.oconf'), ' file. This file is used to configure the environment. Let\'s take an example of your ', _('b', 'Product'), ':',
new CodeBlock({
lines: [
'"name": "Product"',
'"description": "This is my environment"',
'"version": "1.0.0"',
'"autoload": [',
' "src/"',
']',
]
}),
'Let\'s explain the configuration:',
_('ul', [
_('li', [
_('b', 'name'), ': The name of the environment.'
]),
_('li', [
_('b', 'description'), ': The description of the environment.'
]),
_('li', [
_('b', 'version'), ': The version of the environment.'
]),
_('li', [
_('b', 'autoload'), ': This is an array of directories to autoload. This means that when an other environment will use your environment, it will be able to use the classes of your environment. Note that the directories must be relative to the environment directory.'
]),
]),
]),
_('h2', 'Conclusion'),
_('p', [
'In this tutorial, you have seen what is an environment and how to create one. You have also seen how to communicate with other environments and how to create dynamic endpoints. In the next tutorial, you will see more about Ophose itself.'
])
]);
}
};
;
;
class TutorialOphose extends Tutorial {
constructor() {
super({});
this.setTitle('Ophose');
this.setDescription('Finally, everything wouldn\'t be possible without Ophose. Let\'s see how to use it and its main functionalities such as for deployment, build...');
this.setContent([
_('h2', 'Ophose configuration'),
_('p', [
'Ophose is a framework that allows you to create web applications easily and if you have read the previous tutorials, you have already understood that. But how to properly configure Ophose ?',
'Ophose is configured in the ', _('b', 'project.oconf'), ' file. This file is located at the root of your project. Let\'s see how to configure it:',
new CodeBlock({
lines: [
'"name": "My project",',
'"production_mode": false,',
'"api_key": "YOUR_OPHOSE_API_KEY",',
'"project_id": "YOUR_OPHOSE_PROJECT_ID",',
'"build": {',
' "redirect": {',
' "force_https_redirect": true,',
' }',
'}',
'"dependencies": {',
' "publisher/dependency": "version"',
'}',
],
language: 'json'
}),
'Let\'s see what each parameter means:',
_('ul', [
_('li', [
_('b', 'name'), ' is the name of your project. It will be displayed in the browser tab for example by default.'
]),
_('li', [
_('b', 'production_mode'), ' is a boolean that indicates if your project is in production mode. If it is, the project will be built and deployed in production mode. Otherwise, it will be built and deployed in development mode.'
]),
_('li', [
_('b', 'api_key'), ' is the API key of your project. You can find it in your account settings. It is used to install dependencies.'
]),
_('li', [
_('b', 'project_id'), ' is the ID of your project. It is useful to avoid conflicts if you have multiple projects on the same server.'
]),
_('li', [
_('b', 'build.redirect'), ' is the redirection configuration.'
]),
_('li', [
_('b', 'build.redirect.force_https_redirect'), ' is a boolean that indicates if the project must redirect to HTTPS. If it is, the project will redirect to HTTPS if it is not already in HTTPS.'
]),
_('li', [
_('b', 'dependencies'), ' is the Ophose dependencies configuration list. It contains all Ophose dependencies you need.'
]),
_('li', [
_('b', 'dependencies.*'), ' is the Ophose dependency. You just need to follow pattern: ', _('b', 'publisher/dependency'), ' where ', _('b', 'publisher'), ' is the publisher of the dependency and ', _('b', 'dependency'), ' is the dependency name and ', _('b', 'version'), ' is the dependency version.'
])
]),
]),
_('h2', 'Main Ophose commands'),
_('p', [
'To run Ophose commands, you must use the ', _('b', 'ocl'), ' PHP file. Simply run it with PHP and the command you want to execute. For example, to install dependencies, you must run ', _('b', 'php ocl install'), '.',
_('br'),
_('br'),
_('h3', 'Available commands'),
'Here is the list of all available commands:',
_('ul', [
_('li', [
_('b', 'install'), ' installs all dependencies of the project.'
]),
_('li', [
_('b', 'build'), ' builds the project.'
])
]),
]),
_('h2', 'Developing and publishing an Ophose dependency'),
_('p', [
'Ophose allows you to create and publish your own dependencies on the store. To do so, you must follow some steps. Let\'s see how to do it.',
_('h3', 'Creating a component'),
'Make sure to place all your components JS files in the folder at ', _('b', '/components/.ext/{author}/{resource-name}'), '. You can put as many javascript components in it. For example, if you want to create components for a library named ', _('b', 'Form'), ', you must create a folder at ', _('b', '/components/.ext/{author}/Form'), '. Then you can create your components in it such as', _('b', 'Form.js'), ', ', _('b', 'FormInput.js'), '...',
_('br'),
_('br'),
_('h3', 'Creating an environment'),
'To create an environment, you must create a folder at ', _('b', '/env/.ext/{author}/{resource-name}'), '. You can put as many environment files in it. For example, if you want to create an User environment, you must create a folder at ', _('b', '/env/.ext/{author}/User'), '. In this folder at the root, you must create a file named ', _('b', 'env.oconf'), '. This file will contain the environment configuration.', _('br'),
new CodeBlock({
lines: [
'"name": "User",',
'"description": "User environment",',
'"version": "1.0.0",',
'"dependencies": {',
' "publisher/dependency": "version"',
'},',
'"autoload": [',
' "src/"',
' "src/commands/',
']'
],
language: 'json'
}),
_('br'),
'To explain the configuration, here is what each parameter means:',
_('ul', [
_('li', [
_('b', 'name'), ' is the name of the environment.'
]),
_('li', [
_('b', 'description'), ' is the description of the environment.'
]),
_('li', [
_('b', 'version'), ' is the version of the environment.'
]),
_('li', [
_('b', 'dependencies'), ' is the list of dependencies of the environment.'
]),
_('li', [
_('b', 'autoload'), ' is the list of folders to autoload. If you have a class named ', _('b', 'User'), ' in the folder ', _('b', 'src/'), ' and a class named ', _('b', 'UserCommand'), ' in the folder ', _('b', 'src/commands/'), ' you must add these folders in the autoload list. So every other environment can use your classes. Note that the namespace of your classes must be ', _('b', '{author}\\{resource-name}'), '.'
]),
]),
'You can also create a ', _('b', 'env.php'), ' file in the folder to add commands, endpoints, and more... See reference for more information ', new Link({href: '/docs/back-environment', children: 'here'}), '.',
'You can also create a ', _('b', 'env.js'), ' file in the folder to add javascript code. To load it in your application, call the .', _('b', 'oimpe("{author}/{environment-name}")'), ' in your JavaScript code. And add other JS files in the ', _('b', 'js/'), ' folder. To call them, simply call the .', _('b', 'oimpe("{author}/{environment-name}/{file-name}")'), ' in your JavaScript code.',
_('br'),
_('br'),
_('h3', 'Publishing your resource'),
'To publish your resource, you must create a ZIP file containing the ', _('b', '/components/.ext/{author}/{resource-name}'), ' or ', _('b', '/env/.ext/{author}/{resource-name}'), ' folders. Then simply go to the Ophose store and click on the button to publish a new resource. You will have to fill in some information and upload your ZIP file. Then you can publish it.',
])
]);
}
}class SwitchThemeButton extends Ophose.Component {
constructor() {
super();
}
render() {
return new PlacedLive(appLives.GLOBAL_THEME, (theme) => {
return {_: 'i', style: 'cursor: pointer;', className: `bi bi-${theme === 'light' ? 'sun' : 'moon-fill'}`, onclick: () => {
appLives.GLOBAL_THEME.set(theme === 'dark' ? 'light' : 'dark');
}}
});
}
}class Blocker extends Ophose.Component {
constructor(props) {
super(props);
}
style() {
return /*css*/`
%self {
position: absolute;
}
%self .blocker-bg {
position: fixed;
background-color: rgba(0, 0, 0, 0.25);
top: 0;
left: 0;
width: 100%;
height: 100vh;
z-index: 1000;
}
%self > *:not(.blocker-bg) {
z-index: 1001;
position: absolute;
}
`
}
render() {
return {
_: 'div',
id: 'blocker',
children: [
{_:'div', className: 'blocker-bg', onclick: this.props.onClose},
this.props.children
]
}
}
};
;
class HeaderProfile extends Ophose.Component {
constructor(props) {
super(props);
this.menuOpen = new Live(false);
}
style() {
return /* css */`
/* Profile button */
%self .profile_button {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 2rem;
}
%self .profile_button:hover {
background-color: var(--comp-bg-color);
}
%self .profile_button img {
width: 2rem;
height: 2rem;
border-radius: 50%;
}
/* Profile dropdown */
%self .profile_dropdown {
background-color: var(--comp-bg-color);
border-radius: 0.5rem;
overflow: hidden;
right: -4rem;
top: 1rem;
border: var(--comp-border);
width: 20rem;
}
%self .profile_dropdown_header {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
padding: 1rem;
}
%self .profile_dropdown_header img {
width: 3rem;
height: 3rem;
border-radius: 50%;
}
%self .profile_dropdown_header_info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
/* Profile dropdown body */
%self .profile_dropdown_body {
padding: 0.5rem 1rem;
border-top: var(--comp-border);
}
%self .profile_dropdown_body a {
display: block;
padding: 0.5rem 0;
color: var(--text-color);
transition: 0.1s;
font-size: 0.9rem;
}
%self .profile_dropdown_body a:hover {
color: var(--text-color-light);
}
`
}
close() {
this.menuOpen.set(false);
}
render() {
let createLink = (href, text, icon, onClick = undefined, style = undefined) => new Link({href: href, children: [
_('span', {className: 'bi bi-' + icon, style: 'margin-right: 0.5rem;'}),
_('span', text)
], style: 'position:relative;' + style ?? '', onclick: () => {
this.close();
if(onClick) onClick();
}});
return {_: 'div', children: [
_('div', {className: 'profile_button', onclick: () => this.menuOpen.set(!this.menuOpen.value)},
_('img', {src: '/@/ah4/user/image', style: ''}),
_('a', {className: 'bi bi-caret-down-fill'})
),
new PlacedLive(this.menuOpen, (open) => {
return open &&
new Blocker({onClose: () => this.menuOpen.set(false), children: [
_('div', {className: 'profile_dropdown'},
_('div', {className: 'profile_dropdown_header'},
_('img', {src: '/@/ah4/user/image'}),
_('div', {className: 'profile_dropdown_header_info'},
_('b', {className: 'profile_dropdown_header_info_name'}, appLives.USER.value.username),
_('span', {style: 'color: var(--text-color-light);'}, '@' + appLives.USER.value.username),
new Link({href: '/my', children: _('span', 'View profile'), onclick: this.close.bind(this)}),
)
), // header
_('div', {className: 'profile_dropdown_body'},
createLink('/my/api', 'Your API', 'bi bi-key-fill'),
createLink('/my/owned-resources', 'Owned resources', 'bi bi-puzzle-fill'),
createLink('/my/watchlist', 'Watchlist', 'bi bi-eye-fill'),
createLink('/my/notifications', 'Notifications', 'bi bi-bell-fill'),
createLink('/my/resources', 'Your resources', 'bi bi-folder-fill'),
), // body
_('div', {className: 'profile_dropdown_body'},
createLink('/my/settings', 'Settings', 'gear-fill'),
_('a', {onclick: () => {
User.logoutFrom(appLives.USER);
this.close();
}},
_('span', {className: 'bi bi-box-arrow-left', style: 'margin-right: 0.5rem;'}),
_('span', "Logout")
)
)
) // dropdown
]}) // blocker
}) // placed live
]} // self
}
};
;
;
class Header extends Ophose.Component {
constructor() {
super();
this.currentLink = new Live(window.location.pathname);
Ophose.Event.addListener("onPageLoaded", (requestUrl) => {
setTimeout(() => {
this.currentLink.set(window.location.pathname);
}, 10);
})
this.links = [
this.createPageLink('/', 'Home', /^\/$/),
this.createPageLink('/#features', 'Features', /^\/#features$/),
this.createPageLink('/tutorials/get-started', 'Tutorials', /^\/tutorials/),
this.createPageLink('/docs', 'Docs', /^\/docs/),
this.createPageLink('/store', 'Store', /^\/store/),
];
this.opennedMobileMenu = new Live(false);
}
style() {
return /*css*/`
%self {
z-index: 100;
width: 100%;
position: sticky;
top: 0;
border-bottom: var(--comp-border);
background-color: var(--bg-color);
backdrop-filter: blur(10px);
height: fit-content;
}
%self .content {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.5rem 0;
gap: 3rem;
}
%self .logo,
%self .logo_image {
font-size: 1.5rem;
color: var(--text-color);
}
%self a {
font-size: 0.9rem;
color: var(--text-color-light);
font-family: var(--font-js);
text-decoration: none;
font-weight: 500;
letter-spacing: 0.05rem;
transition: 0.2s;
user-select: none;
cursor: pointer;
}
/* Links */
%self .links {
display: flex;
flex-direction: row;
gap: 1rem;
margin-left: auto;
}
%self .links a:hover {
color: var(--text-color);
}
/* Actions */
%self .actions {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
}
%self .actions > div{
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
}
%self .dev_banner {
background-color: var(--comp-bg-color);
color: var(--text-color);
text-align: center;
padding: 0.25rem 0;
font-size: 0.75rem;
}
%self .active {
color: var(--text-color);
}
%self .blocker {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 100;
background-color: rgba(0, 0, 0, 0.5);
}
%self .list {
display: none !important;
}
`
}
styles() {
return {
'md': /* css */`
%self .content {
justify-content: space-between;
}
%self .logo {
display: none;
}
%self .mobile_menu {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 2rem;
position: fixed;
border-radius: 0.5rem;
top: 50vh;
left: 50vw;
transform: translate(-50%, -50%);
z-index: 101;
background-color: var(--bg-color);
overflow-y: auto;
}
/* Links */
%self .links {
display: none;
}
%self .links a:hover {
color: var(--text-color);
}
%self .hid_logo {
display: none;
}
/* Actions */
%self .actions {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
}
%self .actions > div{
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
}
%self .dev_banner {
background-color: var(--comp-bg-color);
color: var(--text-color);
text-align: center;
padding: 0.25rem 0;
font-size: 0.75rem;
}
%self .active {
color: var(--text-color);
}
%self .list {
display: flex !important;
}
`
}
}
createPageLink(path, name, regExp) {
return new Link({href: path, c: new PlacedLive(this.currentLink, (link) => {
return link.match(regExp) !== null ? _('b', {style: 'color: var(--text-color);'}, name) : name;
})});
}
render() {
return _('header', [
_('div', {className: 'dev_banner'}, [
_('p', 'This framework is still under development. Please report any bugs on GitHub.'),
]),
_('div', {className: 'wrapper content' }, [
new Link({href: '/', className: 'logo_image', c: [
_('img', {src: '/img/ophose logo.png', style: 'width: 2rem; height: 2rem;'}),
]}),
new Link({href: '/', className: 'hid_logo', c: _('h1', {className: 'logo'}, 'OPHOSE')}),
_('div', {className: 'links'}, this.links),
_('div', {className: 'actions'}, [
new PlacedLive(appLives.USER, (user) => {
return user == null ? _('div',
new Link({className: 'button', c: 'Register', href: '/auth/register'}),
new Link({c: 'Login', href: '/auth/login'}),
) : new HeaderProfile();
}),
new SwitchThemeButton(),
new Link({href: 'https://github.com/ah-4/ophose-release', c: _('i', {className: 'bi bi-github'})}),
_('div', {className: 'list bi bi-list', onclick: () => {
this.opennedMobileMenu.toggle();
}}),
new PlacedLive(this.opennedMobileMenu, (openned) => {
return openned && _('div',
_('div', {className: 'blocker', onclick: () => {
this.opennedMobileMenu.set(false);
}}),
_('div', {className: 'mobile_menu'},
this.links
)
);
})
]),
])
])
}
};
class Footer extends Ophose.Component {
constructor() {
super();
}
style() {
return /* css */`
%self {
background-color: var(--bg-color-light);
border-top: var(--comp-border);
width: 100%;
}
%self .content {
display: flex;
padding: 2em 0;
font-size: 0.75rem;
}
%self .links {
display: flex;
gap: 1em;
margin-top: 1em;
flex-direction: column;
}
%self .logo {
font-size: 1.125em;
font-weight: 800;
color: var(--logo-color);
text-shadow: var(--text-shadow);
font-family: var(--font-pholioo);
}
`
}
styles() {
return {
'md': /* css */`
%self .content {
flex-direction: column;
text-align: center;
}
%self .logo {
font-size: 1.125em;
font-weight: 800;
color: var(--logo-color);
text-shadow: var(--text-shadow);
font-family: var(--font-pholioo);
}
`
}
}
render() {
return _('footer', [{
_: 'div', className: 'wrapper content', c: [
_('div', {className: 'links', style: 'flex: 4;'}, [
_('h2', {className: 'logo'}, 'OPHOSE'),
_('p', [
'ophose is a free and open-source web framework made by ',
_('a', {href: 'https://ah4.fr/'}, '©AH4'),
'.'
]),
_('p', 'This website is made with ophose. It is an open-source project.')
]),
_('div', {className: 'links', style: 'flex: 2;'}, [
_('h2', 'General'),
new Link({href: '/', c: 'Home'}),
new Link({href: '/about', c: 'About'}),
new Link({href: '/#features', c: 'Features'}),
new Link({href: 'https://github.com/ah-4/ophose-release', c: 'Get / Download Ophose'}),
new Link({href: '/licence', c: 'Licence'}),
new Link({href: 'https://ah4.fr/', c: 'Author'}),
]),
_('div', {className: 'links', style: 'flex: 2;'}, [
_('h2', 'Tutorials'),
new Link({href: '/tutorials/get-started', c: 'Get started'}),
new Link({href: '/tutorials/components', c: 'Components'}),
new Link({href: '/tutorials/pages', c: 'Pages'}),
new Link({href: '/tutorials/environments', c: 'Environments'}),
new Link({href: '/tutorials/ophose', c: 'Framework'}),
new Link({href: '/tutorials', c: 'All tutorials'}),
]),
_('div', {className: 'links', style: 'flex: 2;'}, [
_('h2', 'Documentation'),
new Link({href: '/docs/front-components', c: 'Components'}),
new Link({href: '/docs/front-live', c: 'Live'}),
new Link({href: '/docs/front-pages', c: 'Pages'}),
new Link({href: '/docs/back-environments', c: 'Environments'}),
new Link({href: '/docs/back-commands', c: 'Commands'}),
new Link({href: '/docs', c: 'All documentation'}),
]),
new PlacedLive(appLives.STATS, (stats) => {
if(stats == null) return _('div', 'Loading...');
return _('div', {className: 'links', style: 'flex: 2;'}, [
_('h2', 'Store'),
new Link({href: '/store', c: 'Go to store'}),
_('h2', 'Stats'),
_('p', ['Registered users: ', _('b', stats.users)]),
_('p', ['Shared revenue: ', _('b', '$' + stats.shared_revenue)]),
_('p', ['Downloads: ', _('b', stats.downloads)]),
_('p', ['Version: ', _('b', stats.version)])
])
})
]
}])
}
}class Toast extends Ophose.Component {
static show(props) {
let toast = new Toast(props);
page.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 5000);
}
constructor(props) {
super(props);
}
style() {
return /* css */`
%self {
position: fixed;
bottom: 0;
right: 0;
background-color: var(--comp-bg-color);
opacity: 0.75;
margin: 1rem;
border-radius: 1rem;
border: var(--comp-border);
}
%self:hover {
opacity: 1;
}
%self h5 {
padding: 0.75rem;
font-size: 1rem;
font-family: var(--font-sen);
font-weight: 600;
border-bottom: var(--comp-border);
}
%self .content {
padding: 1rem;
font-family: var(--font-lato);
font-size: 0.75rem;
}
`
}
render() {
return _('div', {className: 'toast'}, [
_('h5', this.props.title),
_('p', {className: 'content'}, this.props.message)
]);
}
}class ErosLanguage {
static currentLanguage = new Live('default');
static language = {};
/**
* Initializes the language
* @param {string} lang the language to load
*/
static init(lang) {
oenv('eros/lang/get/' + lang)
.then(language => {
ErosLanguage.language = language;
ErosLanguage.currentLanguage.set(lang);
})
}
/**
* Returns a translation
* @param {*} translationKey the translation key
* @param {*} parameters the parameters to replace in the translation
* @returns {string} the translation
*/
static getTranslation(translationKey, parameters) {
let keys = translationKey.split('.');
let translation = ErosLanguage.language;
for(let key of keys) {
translation = translation[key] ?? undefined;
if(!translation) break;
}
if(!translation) return ':' + translationKey + ':';
if(parameters) {
for(let key in parameters) {
translation = translation.replace('{' + key + '}', parameters[key]);
}
}
return translation;
}
/**
* Places a translation
*
* @param {string} translationKey the translation key
* @param {Array} parameters the parameters to replace in the translation
* @returns {PlacedLive} the placed translation
*/
static placeTranslation(translationKey, parameters) {
return new PlacedLive(ErosLanguage.currentLanguage, () => {
return ErosLanguage.getTranslation(translationKey, parameters);
})
}
}
const et = ErosLanguage.getTranslation;
const ep = ErosLanguage.placeTranslation;class User {
/**
* Disconnects the user.
*
* @param {Live} live The live to set to null when the user is disconnected.
*/
static logoutFrom(live) {
oenv('ah4/user/logout')
.then((r) => {
live.set(null);
route.go('/');
});
}
/**
* Connects the user.
*
* @param {Live} live The live to set to the user when the user is connected.
*/
static loginInto(live) {
oenv("ah4/user/current")
.then(r => {
live.set(r);
});
}
/**
* Returns if the user is connected.
*
* @returns {boolean} True if the user is connected, false otherwise.
*/
static async isLogged() {
return await oenv("ah4/user/current");
}
static async redirectIfNotLogged(url = '/auth/login') {
User.isLogged()
.catch(() => {
route.go(url);
});
}
static async redirectIfLoggedIn(url = '/') {
User.isLogged()
.then(() => {
route.go(url);
});
}
}const appLives = {
GLOBAL_THEME: Live.local("theme", "dark"),
USER: new Live(null),
STATS: new Live(null)
}
importScript("https://cdn.jsdelivr.net/npm/markdown-it@14.0.0/dist/markdown-it.min.js");
var md = undefined;
;
;
;
;
;
class Base extends Ophose.Base {
constructor(props) {
super(props);
md = window.markdownit();
importCss("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css");
importCss("https://fonts.googleapis.com/css2?family=Lato&family=Sen:wght@400;800&family=Josefin+Sans&family=Press+Start+2P&display=swap");
importCss("/css/ophose.css");
importCss("/css/ophose_forms.css");
importCss("/css/ophose_animations.css");
importCss("/css/prism.css");
importScript("/js/prism.js");
// Language
ErosLanguage.init("en");
// Load user
User.loginInto(appLives.USER);
oenv("info/get_stats")
.then((response) => {
appLives.STATS.set(response);
})
}
style() {
let colors = appLives.GLOBAL_THEME == "light" ? `
--bg-color: #ffffff;
--bg-color-light: #f4f4f4;
--text-color: #333333;
--text-color-light: #777777;
--comp-bg-color: #f9f9f9;
--comp-border-color: #dcdcdc;
--link-color: #007bff;
--super-color: #FFD5B4;
` : `
--bg-color: #111;
--bg-color-light: #222;
--text-color: #ffffff;
--text-color-light: #aaa;
--comp-bg-color: #333;
--comp-border-color: #555;
--link-color: #448756;
--super-color: #431479;
`;
return /* css */`
:root {
--font-lato: 'Lato', sans-serif;
--font-sen: 'Sen', sans-serif;
--font-js: 'Josephine Sans', sans-serif;
--font-ophose: 'Press Start 2P', sans-serif;
/* Colors */
${colors}
--gradient-main: linear-gradient(160deg, #00DBDE 0%, #FC00FF 100%);
/* Components */
--comp-border: 1px solid rgba(127, 127, 127, 0.25);
--border-light: rgba(127, 127, 127, 0.1) 1px solid;
/* Wrapper */
--wrapper-width: 98%;
--wrapper-width-max: 1200px;
}
.wrapper {
width: var(--wrapper-width);
max-width: var(--wrapper-width-max);
margin: 0 auto;
position: relative;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-js);
}
#page {
min-height: 80vh;
}
`
}
render() {
return {
_: 'div', c: [
new Header(),
{
_: 'div',
id: 'page',
children: this.props.children,
},
new Footer()
]
}
}
}