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 HomeSectionWelcome extends Ophose.Component {constructor(props) {super(props);} render() {return _('section', {id: "welcome", className: "border-b border-gray-200"},_('div', {className: "flex items-center justify-center py-48 container mx-auto gap-6 flex-col text-center lg:text-left"},_('h1', {className: "text-7xl font-bold"}, _('span', {className: "text-indigo-500"}, "do less;"), " create more."),_('p', {className: "text-3xl text-gray-500 font-semibold"}, "with ", _('span', {className: "text-indigo-500"}, "Ophose"), " a framework for modern web development."),_('div', {className: "flex gap-4"},_('go', {href: '/tutorial/get-started', className: "px-6 py-3 text-lg bg-indigo-500 text-white rounded-full font-semibold hover:bg-indigo-600"}, "Get Started"),_('go', {href: '/doc', className: "px-6 py-3 text-lg bg-gray-100 text-gray-700 rounded-full font-semibold hover:bg-gray-200"}, "View Documentation")) ));} }class CodeBlock extends Ophose.Component {constructor(props) {super(props);} style() {return /* css */` %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', {className: 'rounded-xl overflow-hidden text-xs p-4'}, [_('code', { className: `language-${this.props.lang ?? 'javascript'}` }, [this.props.lines.join('\n')])])} }oimpc('misc/code/CodeBlock');class HomeSectionComponent extends Ophose.Component {constructor(props) {super(props);} render() {let _alert = (title, message) => _('div', {className: "flex p-4 bg-gray-100 rounded-xl gap-4 shadow-xl h-fit"},_('i', {className: "text-4xl bi bi-check text-green-500"}),_('div', {className: "flex flex-col gap-1"},_('h1', {className: "text-xl font-semibold"}, title),_('p', {className: "text-gray-500"}, message)),_('i', {className: "bi bi-x cursor-pointer"}));return _('section', {id: "welcome", className: "border-b border-gray-200 bg-gradient-to-br from-gray-50 to-gray-100"},_('div', {className: "grid grid-cols-1 lg:grid-cols-2 py-32 container mx-auto gap-8 items-center"},// Description _('div', {className: "flex flex-col gap-4"},_('h1', {className: "text-5xl font-bold"}, 'Build the user interface with reusable components...'),_('p', {className: "text-xl text-gray-500"}, 'Ophose allows you to create your user interface with reusable components. They are small pieces of visual elements that can be reused across your application. Components can be nested inside other components to create complex and rich user interfaces.'),new CodeBlock({lines: ["// Alert.js","render() {"," return _('div',"," new Icon(this.props.icon),"," _('div',"," _('h1', this.props.title),"," _('p', this.props.message)"," ),"," _('button', {onclick: this.props.onClose}, 'X')"," );","}" ]})),// Component _('div', {className: "p-8 rounded-xl bg-gradient-to-br from-indigo-500/20 to-pink-400/20 shadow-xl bg-opacity-20 flex flex-col gap-4"},_alert('Reusable', 'Components can be reused across your application. They are small pieces of visual elements that can be nested inside other components.'),_alert('Nested', 'Components can be nested inside other components to create complex and rich user interfaces.'),) ));} }oimpc('misc/code/CodeBlock');class HomeSectionLive extends Ophose.Component {constructor(props) {super(props);} render() {let _todoList = () => {let input = new Live('');let todos = new Live([]);return _('div', {className: "flex flex-col p-4 bg-gray-100 rounded-xl gap-4 shadow-xl h-fit"},_('div', {className: "flex gap-4"},_('input', {oninput: e => input.set(e.target.value), placeholder: "Enter a todo item...", className: "py-2 px-4 rounded-full flex-1"}),_('button', {onclick: () => todos.add(input.value), className: "px-4 py-2 bg-indigo-500 text-white rounded-full font-semibold hover:bg-indigo-600"}, 'Add')),_('i', {className: "text-sm text-gray-500"}, _('span', {className: "font-semibold"}, 'Input: '), input),dyn(todos, todos => _('ul', {className: "flex flex-col gap-2"},todos.map(todo => _('li', {className: "flex gap-2 items-center font-semibold text-gray-700"},_('i', {className: "bi bi-check text-green-500"}),todo ))))) };return _('section', {id: "welcome", className: "border-b border-gray-200 bg-gradient-to-br from-gray-50 to-gray-100"},_('div', {className: "grid grid-cols-1 lg:grid-cols-2 py-32 container mx-auto gap-8 items-center"},// Component _('div', {className: "p-8 rounded-xl bg-gradient-to-br from-indigo-500/20 to-pink-400/20 shadow-xl bg-opacity-20 flex flex-col gap-4"},_todoList()),// Description _('div', {className: "flex flex-col gap-4"},_('h1', {className: "text-5xl font-bold"}, '...then add some reactivity to them...'),_('p', {className: "text-xl text-gray-500"}, 'Are you loading data from an API? Do you want to update the UI when the data changes? Ophose provides a simple way to add reactivity to your components. You can use the ', _('code', 'Live'), ' class to create dynamic values displayed in your application.'),new CodeBlock({lines: ["// ToDoList.js","render() {"," let input = new Live('');"," let todos = new Live([]);",""," return _('div',"," _('div',"," _('input', {watch: input}),"," _('button', {onclick: () => todos.add(input.value)}, 'Add')"," ),"," _('i', 'Input: ', input),"," _('ul',"," todos._map(todo => _('li', todo))"," )"," );","}" ]})),) );} }oimpc('misc/code/CodeBlock');class HomeSectionFullstack extends Ophose.Component {constructor(props) {super(props);} render() {let _fetchedData = _('div', {className: "flex flex-col bg-gray-100 rounded-xl shadow-xl h-fit p-4 gap-4"},_('p', {className: "py-2 px-4 rounded-full flex-1 select-none text-gray-400 text-center bg-white text-gray-400 font-semibold", readonly: true},"example.com/",_('span', {className: "text-gray-700"}, "api/myenv/posts")),// Fake data as JSON new CodeBlock({lang: 'json', lines: ["["," {"," \"id\": 1,"," \"title\": \"Hello World\","," \"content\": \"This is the first post.\""," },"," {"," \"id\": 2,"," \"title\": \"Second Post\","," \"content\": \"This is the second post.\""," }","]" ]})) return _('section', {id: "welcome", className: "border-b border-gray-200 bg-gradient-to-br from-gray-50 to-gray-100"},_('div', {className: "grid grid-cols-1 lg:grid-cols-2 py-32 container mx-auto gap-8 items-center"},// Description _('div', {className: "flex flex-col gap-4"},_('h1', {className: "text-5xl font-bold"}, '...and make your app go fullstack'),_('p', {className: "text-xl text-gray-500"}, 'This framework also provides a way to handle API requests and responses, and to create a fullstack application with many pre-built modules called ', _('code', 'Environment'), '. They are small pieces of code that can be used to interact with the server, the database and other services.'),new CodeBlock({lang: 'php', lines: ["endpoint('/posts', function() {"," return Response::json(Post::all());"," });"," }","};" ]})),// Component _('div', {className: "p-8 rounded-xl bg-gradient-to-br from-indigo-500/20 to-pink-400/20 shadow-xl bg-opacity-20 flex flex-col gap-4"},_fetchedData ),) );} }oimpc('misc/code/CodeBlock');class HomeSectionWhy extends Ophose.Component {constructor(props) {super(props);} render() {return _('section', {id: "welcome", className: "border-b border-gray-200 bg-gradient-to-br from-gray-50 to-gray-100"},_('div', {className: "flex flex-col py-32 container mx-auto gap-8 items-center text-center"},_('h1', {className: "text-5xl font-bold w-full lg:w-1/2"}, 'Why moving to Ophose?'),_('p', {className: "text-2xl font-semi-bold text-gray-500 w-full lg:w-3/5"}, 'This modern framework has been designed to help you build web applications faster and easier with the fewest requirements and dependencies. Its deployment is simple and straightforward, and it provides a way to create a fullstack application with many environments only waiting for you to use them.'),_('iframe', {src: "https://www.youtube.com/embed/7GFghTu4_R8", width: "560", height: "315", frameborder: "0", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture", allowfullscreen: true, className: "rounded-xl shadow-xl mt-8", style: "width: 48vmax; height: 27vmax;"}),) );} }class ResourceSearch extends Ophose.Component {constructor(props) {super(props);this.resources = live(null);this.totalPages = live(1).min(1);this.page = live(1).min(1).max(this.totalPages);this.search = live('');watch(this.page, this.search, (page, search) => {oenv(this.props.endpoint, {page, query: search}).then(resources => {this.resources.set(resources);this.totalPages.set(this.resources.value.totalPages);});})} render() {let _pagination = _('div', {className: "flex gap-4 justify-between items-center"},_('p', {className: "text-gray-500"}, dyn(this.resources, resources => resources && `Showing ${this.page * resources.perPage - resources.perPage + 1} to ${this.page * resources.perPage} of ${resources.count} resources`)),_('div', {className: "flex"},_('button', {className: "px-3 py-1 bg-gray-200 rounded-full font-semibold hover:bg-gray-300", onclick: () => this.page.remove(1)}, "<"),_('a', {href: "#", className: "px-3 py-1 text-gray-500 font-semibold text-lg"},_('span', {className: "font-bold"}, this.page),"/",_('span', {className: "font-bold"}, this.totalPages),),_('button', {className: "px-3 py-1 bg-gray-200 rounded-full font-semibold hover:bg-gray-300", onclick: () => this.page.add(1)}, ">"),) ) let _resource = (resource) => _('go', {href: '/r/' + resource.id, className: "bg-white rounded-lg p-4 flex gap-4 items-center hover:bg-gray-50 cursor-pointer rounded-lg relative"},_('img', {src: resource.image, alt: resource.title, className: "h-20 w-20 rounded-lg"}),_('div', {className: "flex flex-col gap-2"},_('h2', {className: "text-2xl font-bold"}, resource.title),_('p', {className: "text-gray-500"}, resource.subtitle),_('div', {className: "flex gap-2 items-center"},resource.review.count > 0 && _('div', {className: "flex gap-2 items-center"},_('i', {className: "bi bi-star text-yellow-500"}),_('p', {className: "text-gray-500"}, resource.review.rating.toFixed(1)),_('p', {className: "text-gray-500"}, `(${resource.review.count} reviews)`),) )),_('div', {className: "absolute top-4 right-4 flex gap-4 text-sm font-semibold"},_('a', {className: "bg-indigo-500 text-white py-1 px-2 rounded"}, `${resource.price ? '$' + resource.price : 'Free'}`),_('a', {className: "bg-gray-200 text-gray-500 py-1 px-2 rounded"}, resource.type.charAt(0).toUpperCase() + resource.type.slice(1)),),) // resource return _('div', {className: "w-full grid grid-cols-1 lg:grid-cols-4 gap-8"},// Options panel _('div', {className: "col-span-1 bg-gray-50 rounded-lg p-4 flex flex-col gap-4"},_('div', {className: "flex flex-col gap-2"},_('p', "Search for"),_('input', {placeholder: "Search...", className: "py-2 px-4 rounded-full bg-white border border-gray-200 w-full"})),// Sort filter _('div', {className: "flex flex-col gap-2"},_('p', "Sort by"),_('select', {className: "py-2 px-4 rounded-full bg-white border border-gray-200 w-full"},_('option', {value: "", selected: true}, "Best match"),_('option', {value: "price"}, "Price"),_('option', {value: "review"}, "Review"),_('option', {value: "reviews"}, "Reviews")) ),// Type filter _('div', {className: "flex flex-col gap-2"},_('p', "Type"),_('select', {className: "py-2 px-4 rounded-full bg-white border border-gray-200 w-full"},_('option', {value: "", selected: true}, "All"),_('option', {value: "component"}, "Component"),_('option', {value: "template"}, "Environment")) ),// Price filter _('div', {className: "flex flex-col gap-2"},_('p', "Price"),_('div', {className: "flex gap-2 items-center"},_('input', {type: "number", placeholder: "Min", className: "py-2 px-4 rounded-full bg-white border border-gray-200 w-full flex-"}),_('p', {className: "text-gray-500"}, "-"),_('input', {type: "number", placeholder: "Max", className: "py-2 px-4 rounded-full bg-white border border-gray-200 w-full flex-"})) ),),// Resources list _('div', {className: "lg:col-span-3 flex flex-col gap-4"},_pagination,dyn(this.resources, resources => {if(resources === null) return _('div', {className: "text-center text-gray-500"}, 'Loading...');if(resources.rows.length === 0) return _('div', {className: "text-center text-gray-500"}, 'No resources found.');return _('div', {className: "grid grid-cols-1 lg:grid-cols-2 gap-4"},resources.rows.map(resource => {return _resource(resource);}) // resources.rows.map ) // resources }),_pagination ));} }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]) {if(!Array.isArray(errors[name])) errors[name] = [errors[name]];for(let error of errors[name]) {let li = document.createElement('li');li.textContent = error;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) {dev.error('XForm requires an endpoint');};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.props.value || '');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 UComponent extends Ophose.Component {constructor(props) {super(props);} }oimpc('@/NUI/Main/UComponent');class UTable extends UComponent {constructor(props) {super(props);if(!this.props.mockData && !this.props.endpoint) dev.error("UTable requires either mockData or endpoint prop");this.data = live(null);if(this.props.mockData) {this.data = live({totalPages: 1,count: this.props.mockData.length,rows: this.props.mockData });}else if(this.props.endpoint) {oenv(this.props.endpoint).then((data) => {this.data.set(data);});} this.columns = live(this.props.columns || []);this.totalPages = live(1).min(1);this.page = live(1).min(1).max(this.totalPages);this.search = live('');} render() {let _search = _('div', {className: "flex items-center gap-4"},_('input', {type: "text", placeholder: "Start typing to search...", className: "w-1/3 px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500", watch: this.search}));let _pagination = () => _('div', {className: "flex justify-between items-center gap-4"},_('div', {className: "flex items-center gap-1"},_('span', {className: "text-sm text-gray-700"}, "Showing"),_('span', {className: "text-sm text-gray-700"}, dyn(this.data, (data) => data?.rows.length ?? 0)),_('span', {className: "text-sm text-gray-700"}, "of"),_('span', {className: "text-sm text-gray-700"}, dyn(this.data, (data) => data?.count)),_('span', {className: "text-sm text-gray-700"}, "results"),),_('div', {className: "flex items-center gap-2"},_('button', {className: "text-sm text-gray-700"}, "<<"),_('button', {className: "text-sm text-gray-700"}, "Previous"),_('span', {className: "text-sm text-gray-700"}, this.page),_('span', {className: "text-sm text-gray-700"}, "/"),_('span', {className: "text-sm text-gray-700"}, this.totalPages),_('button', {className: "text-sm text-gray-700"}, "Next"),_('button', {className: "text-sm text-gray-700"}, ">>"),) );let _table = _('table', {className: "table-auto w-full border-collapse border border-gray-200 bg-white rounded-xl border-spacing-0 overflow-hidden"},// Table Head _('thead', {className: "bg-gray-50 text-left text-sm font-semibold"},dyn(this.columns, (columns) => {return _('tr',columns.map((column, index) => {return _('th', {key: index, className: "px-6 py-4"}, column.title);}));})),// Table Body dyn(this.data, (data) => {if(!data) return _('tbody',_('tr',_('td', {colspan: "100%", className: "px-6 py-4 border-t border-gray-200 font-semibold text-center"},"Loading..." )) );if(data.count == 0) return _('tbody',_('tr',_('td', {colspan: "100%", className: "px-6 py-4 border-t border-gray-200 font-semibold text-center"},"No results found" )) );return _('tbody',data.rows.map((row, rowIndex) => {return _('tr', {key: rowIndex, className: ""},this.columns.value.map((column, columnIndex) => {return _('td', {key: columnIndex, className: "px-6 py-4 border-t border-gray-200 font-semibold"},(this.props.formatters && this.props.formatters[column.name] && this.props.formatters[column.name](row[column.name], column.name, row, data)) ?? row[column.name]);}));}) // tbody ); // tbody }));return _('div', {className: "flex flex-col gap-4"},_search,_pagination(),_table,_pagination()) }}oimpc('@/AH4/Starter/XForm');oimpc('@/AH4/Starter/XInput');oimpc('@/NUI/Fast/UTable');class ResourceEdit extends Ophose.Component {constructor(props) {super(props);this.idIsDefined = this.props.editId !== undefined ? true : undefined;} style() {return /* css */` %self .char_counter { text-align: right; display: block; } %self .errors { color: #e53e3e; } %self label { font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.5rem; } ` } render() {return new XForm({className: "grid grid-cols-2 gap-4",endpoint: this.props.editId ? 'resources/edit/' + this.props.editId : 'resources/save',dataEndpoint: this.props.editId && 'resources/get/' + this.props.editId,onSuccess: (r) => {route.go('/r/' + r.id)},c: [// Title _('div', { className: "col-span-2" },new XInput({name: "title",label: "Title",required: !this.idIsDefined,disabled: this.idIsDefined,placeholder: "Enter a title",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500",maxlength: 32 })),// Subtitle _('div', { className: "col-span-2" },new XInput({name: "subtitle",label: "Subtitle",required: !this.idIsDefined,placeholder: "Enter a subtitle",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500",maxlength: 128 })),// Description _('div', { className: "col-span-2" },new XInput({name: "description",label: "Description",type: "textarea",required: !this.idIsDefined,placeholder: "Enter a description",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500 h-32",maxlength: 2000,})),// Picture _('div', { className: "flex items-center" },_('img', { className: "w-16 h-16 rounded-lg mr-4", src: (!this.idIsDefined ? "https://via.placeholder.com/150" : ("/api/resources/image/" + this.props.editId)) }),new XInput({name: "picture",label: "Picture",type: "file",required: this.idIsDefined ? undefined : true,className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500",accept: "image/*" })),// Documentation file _('div', { className: "flex items-center" },_('a', { className: "bi bi-file text-3xl mr-4" }),new XInput({name: "documentation",label: "Documentation file",type: "file",required: this.idIsDefined ? undefined : true,className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500",accept: ".json" })),// Price new XInput({name: "price",label: "Price (USD)",placeholder: "Enter a price",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500",type: "number",value: 0,step: ".01" }),// Type new XInput({name: "type",label: "Type",type: "select",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500",required: !this.idIsDefined,disabled: this.idIsDefined,options: [{ "value": 1, "text": "Component" },{ "value": 2, "text": "Environment" }] }),// All versions _('div', { className: "col-span-2 flex flex-col gap-4 my-8" },_('h3', { className: "text-lg font-semibold" }, "All versions"),_('div', { className: "grid grid-cols-1 lg:grid-cols-2 gap-4" },this.idIsDefined && new UTable({actions: [_('button', { className: "bg-red-500 text-white py-1 px-2 rounded-md hover:bg-red-600" }, "Delete"),_('button', { className: "bg-blue-500 text-white py-1 px-2 rounded-md hover:bg-blue-600" }, "Edit"),],endpoint: 'resources/versions/' + this.props.editId,columns: [{ name: "version", title: "Version" },{ name: "description", title: "Description" },{ name: "created_at", title: "Created at" }],formatters: {date: (date) => new Date(date).toLocaleDateString()} }),_('div', { className: "p-4 rounded-xl flex flex-col gap-4 bg-gray-50 " + (!this.idIsDefined && "col-span-2") },_('h3', { className: "text-lg font-semibold" }, "Publish a version"),new XInput({name: "version_name",label: "Version name",placeholder: "Enter a version name",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500",maxlength: 32 }),new XInput({name: "version_description",label: "Version description",placeholder: "Enter a version description",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500",type: "textarea",maxlength: 2000 }),// Resource file _('div', { className: "flex items-center" },_('a', { className: "bi bi-upload text-3xl mr-4" }),new XInput({name: "version_file",label: "Version file",type: "file",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500",accept: ".zip" })),),) ),// Terms _('div', { className: "col-span-2 flex flex-col gap-2 my-8" },_('h3', { className: "text-lg font-semibold" }, "Terms and Conditions"),_('p', { className: "text-sm text-gray-500" }, "By submitting this form, you agree to our Terms of Service and Privacy Policy.")),// Submit buttons (Draft and Submit) _('button', { className: "bg-indigo-500 text-white py-2 px-4 rounded-md hover:bg-indigo-600 col-start-20", type: "submit" }, "Save"),] })} }class PaymentButton extends Ophose.Component {constructor(props) {super(props, {paymentEndpoint: null,paymentData: null });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, this.props.paymentData) .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.props.onPaymentFail && this.props.onPaymentFail(res);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);this.props.onPaymentFail && this.props.onPaymentFail(data);document.body.removeChild(blocker);});} onPaymentComplete() {if(this.props.onCompleteUrl) {route.go(this.props.onCompleteUrl);return;} window.location.reload();} render() {return {_: 'button', onclick: () => this.pay(), children: [this.props.children ]}} }class SEO {static info = {} /** * Update the SEO environment * * @param {Object} newInfo the new SEO information */ static update(newInfo) {let info = {...SEO.info, ...newInfo} for (let key in info) {switch(key) {case 'title': document.title = info.callbacks.title(info.title);break;case 'description': let metaDescription = document.querySelector('meta[name="description"]');if(!metaDescription) {metaDescription = document.createElement('meta');metaDescription.name = "description";document.head.appendChild(metaDescription);} metaDescription.content = info.description;break;case 'favicon': let linkFavicon = document.querySelector('link[rel="icon"]');if(!linkFavicon) {linkFavicon = document.createElement('link');linkFavicon.rel = 'icon';linkFavicon.href = '/favicon.ico';linkFavicon.type = 'image/x-icon';linkFavicon.id = 'favicon';document.head.appendChild(linkFavicon);} linkFavicon.href = info.favicon;break;case 'lang': document.documentElement.lang = info.lang;break;case 'callbacks': if(!SEO.info.callbacks) SEO.info.callbacks = {};for (let callback in info.callbacks) {SEO.info.callbacks[callback] = info.callbacks[callback];} break;default: if(!project.productionMode) {console.warn(`SEO: Unknown key ${key}`);} break;} } SEO.info = info;} } SEO.update({title: "Welcome",description: "Project description",favicon: '/favicon.ico',lang: "en",callbacks: {title: (title) => title + " | " + project.name,} });oimpc('@/AH4/Starter/Link');class Explain extends Ophose.Component {constructor(props) {super(props);} onPlace(element) {let headingsParent = element.querySelector('#headings');let headings = element.querySelectorAll('#content h2');for(let i = 0; i < headings.length; i++) {let heading = headings[i];let headingId = heading.innerText.toLowerCase().replace(/ /g, '-');heading.id = headingId;let a = _('a', {href: "#" + headingId, className: "hover:underline px-8"}, heading.innerText);headingsParent.appendChild(Ophose.Render.toNode(a));} } render() {// _('a', {href: "#"}, "Getting started"),// _('a', {href: "#"}, "Installation"),// _('a', {href: "#"}, "Your first component"),// _('a', {href: "#"}, "Adding interactivity"),// _('a', {href: "#"}, "Application layout"),// _('a', {href: "#"}, "Create pages"),// _('a', {href: "#"}, "Logic and data"),// _('a', {href: "#"}, "Go fullstack") let currentPage = this.props.pages[0];for(let i = 0; i < this.props.pages.length; i++) {console.log(this.props.pages[i].props.identifier, this.props.urlQueries.query.tutorialName);if(this.props.pages[i].props.identifier == this.props.urlQueries.query.tutorialName) {currentPage = this.props.pages[i];break;} } return _('div', {className: "w-full grid grid-cols-4 lg:grid-cols-5 gap-4 lg:gap-16 py-8"},// Pages _('div', {className: "flex flex-col text-gray-700 font-semibold sticky top-24 h-screen"},_('h2', {className: "text-sm font-bold text-gray-500 uppercase py-2 px-8"}, this.props.title),this.props.pages.map((page) => new Link({href: '/tutorial/' + page.props.identifier,className: "py-3 px-8 text-md rounded-r-2xl hover:bg-gray-100 " + (page.props.identifier == currentPage.props.identifier ? 'bg-indigo-100 text-indigo-500' : ''),children: page.props.title }))),// Content _('div', {className: "flex flex-col gap-8 text-lg text-gray-800 col-span-3 w-4/5 mx-auto"},_('h1', {className: "text-5xl font-bold text-gray-700"}, currentPage.props.title),_('p', {className: "text-2xl font-semibold"}, currentPage.props.description),_('div', {id: "content"},currentPage )),// Headings _('div', {className: "lg:flex flex-col text-gray-700 font-semibold gap-2 sticky top-24 h-screen hidden", id: "headings"},_('h2', {className: "text-sm font-bold text-gray-500 uppercase py-2 px-8"}, "Headings")),);} }oimpc('@/AH4/Starter/Link');oimpc('misc/code/CodeBlock');class ExplainContent extends Ophose.Component {constructor(props) {super(props);} style() {return /* css */` %self h2 { font-size: 1.75rem; margin-top: 4rem; margin-bottom: 1rem; font-weight: 700; } %self p { margin-bottom: 1rem; } %self pre { margin-bottom: 2rem; margin-top: 2rem; } %self code { font-size: 1rem; } %self ul { margin-bottom: 2rem; margin-left: 2rem; list-style-type: disc; display: flex; flex-direction: column; gap: 1rem; } %self b { font-weight: 600; display: inline-block; padding: 0.125rem 0.25rem; background-color: rgba(127, 127, 127, 0.1); border-radius: 0.25rem; margin-bottom: 0.125rem; } %self a[href] { color: #2563EB; text-decoration: none; } %self a[href]:hover { text-decoration: underline; } %self .alert { background-color: rgba(255, 229, 100, 0.2); color: rgba(127, 127, 127, 0.8); padding: 1rem; border-radius: 0.5rem; margin-top: 2rem; margin-bottom: 2rem; font-weight: 600; } ` } render() {return _('div',);} codeWithExample(code, example) {return _('div', {className: "grid grid-cols-1 lg:grid-cols-2 items-center lg:-p-8"},new CodeBlock({lines: code}),_('div', {className: "rounded-r-2xl p-4 bg-gradient-to-br from-indigo-500/10 to-indigo-500/20"},_('div', {className: "p-4 rounded-2xl bg-white"},example )) );} browser(url, children) {return _('div', {className: "flex flex-col bg-gray-100 rounded-xl shadow-xl h-fit p-4 gap-4 mb-8"},_('p', {className: "py-2 px-4 rounded-full flex-1 select-none text-gray-400 text-center bg-white text-gray-400 font-semibold", readonly: true},"example.com/",_('span', {className: "text-gray-700"}, url)),// Fake data as JSON _('div', {className: "rounded-2xl bg-white p-4"},children )) }}oimpc('misc/xpl/ExplainContent');class TutoGettingStarted extends ExplainContent {constructor() {super({title: "Getting Started",description: "Are you new to Ophose? This tutorial will guide you through the basics of it. At the end of this tutorial, you will have a good understanding of how this framework works and how to use it to build your web applications.",identifier: 'getting-started' });} render() {let count = live(0);let countSeveral = live(0);let todos = live([]);let todo = live('');return _('div',// Create components _('h2', 'Create components'),_('p', 'The user interface of your application is built using components. A component is a reusable piece of code that can be used in multiple places of your application and even in other components. Each component has its own HTML structure described generally in Ophose format.'),_('p', 'For example, for the following HTML code:'),new CodeBlock({lang: 'html', lines: ["" ]}),_('p', 'You can simply create a component by returning a JS object'),new CodeBlock({lines: ["class MyButton extends Ophose.Component {"," render() {"," return {_: 'button', onclick: () => alert('Clicked'), children: 'Click me'};"," }","}",]}),_('p', 'But using this syntax can be a bit verbose. That\'s why Ophose provides a shorthand to create components'), new CodeBlock({lines: [ "render() {", " return _('button', {onclick: () => alert('Clicked')},", " 'Click me'"," );","}",]}),_('p', 'They are equivalent. The second syntax is more concise and easier to read.'),_('p', 'You can also use this component in another component by simply calling it:'), this.codeWithExample([ "oimpc('MyButton');","","class MyComponent extends Ophose.Component {"," render() {"," return _('div',"," _('p', 'Hello world, my brother is'),"," new MyButton()"," );"," }","}",], _('div',_('p', 'Hello world, my brother is'), _('button', {onclick: () => alert('Clicked'), className: "bg-indigo-500 text-white py-2 px-4 rounded-lg"}, 'Click me'))),_('p', 'The previous returned objects are called Ophose objects. Typically,'), _('ul', _('li', _('b', '_:'), ' is the type of the element (button, div, h1, etc.)' ), _('li', _('b', 'children:'), ' is the children of the element. You can pass multiple children in an array or a single child as a string.' ), _('li', _('b', 'className:'), ' is the class of the element. You can pass a string.' ), _('li', _('b', '[other properties]:'), ' they are the properties of the element. Note that the properties work exactly the same way as in HTML elements. But with the difference that you can use JavaScript expressions in them and pass custom properties.' ),),// Passing properties _('h2', 'Passing properties'),_('p', 'You can pass properties to your components. This is useful when you want to make your components dynamic. Also, contrary to other frameworks, as they are JavaScript objects, you can pass any type of data to them, call them in both the properties and the children, and even pass functions.'),_('p', 'To pass properties to a component, you can simply add them in the second parameter of the component call:'),this.codeWithExample(["class MyComponent extends Ophose.Component {"," render() {"," return _('div',"," _('p', 'Hello ' + this.props.name),"," _('p', 'You are ' + this.props.age + ' years old')"," );"," }","}","","new MyComponent({name: 'John', age: 25})" ], _('div', {className: "font-semibold"},_('p', 'Hello John'),_('p', 'You are 25 years old'))),_('p', 'In the previous example, we passed the properties ', _('b', 'name'), ' and ', _('b', 'age'), ' to the component ', _('b', 'MyComponent'), '. We accessed them in the ', _('b', 'render'), ' method using ', _('b', 'this.props.name'), ' and ', _('b', 'this.props.age'), '.'),// Make it interactive _('h2', 'Make it interactive'),_('p', 'You can make your components interactive by using ', _('b', 'Live'), ' objects. A Live object is an object that can be updated and will automatically update the component that uses it. This is useful when you want to update the component when a value changes.'),_('p', 'To create a Live object, you can use the ', _('b', 'live'), ' function and update it using the ', _('b', 'set'), ' method:'),new CodeBlock({lines: ["render() {"," let count = live(0);",""," return _('div',"," _('p', 'You clicked ', count, ' times'),", " _('button', {onclick: () => count.add(1)}, 'Click me')"," );","}",]}),_('div', {className: "grid grid-cols-2 items-center gap-4 mb-16"},_('a', {className: "text-xl font-extrabold text-gray-500"}, 'You clicked ', _('span', {className: "text-indigo-500"}, count), ' times' ), _('button', {onclick: () => count.add(1), className: "bg-indigo-500 text-white py-2 px-4 rounded-lg"}, 'Click me')),_('p', 'As the ', _('b', 'count'), ' object is a Live object, the component will automatically update when the value changes whatever the place and how many times it is used.'),new CodeBlock({lines: ["class Counter extends Ophose.Component {"," render() {"," return _('button', {onclick: () => this.props.count.add(1)}, 'Click me');"," }","}","","let count = live(0);","","new Counter({count})","new Counter({count})","new Counter({count})" ]}),_('p', 'In the previous example, we created a ', _('b', 'Counter'), ' component that increments the ', _('b', 'count'), ' Live object when clicked. We used this component three times. When you click on one of the buttons, all the counters will update.'),_('div', {className: "grid grid-cols-3 gap-4"},_('button', {onclick: () => countSeveral.add(1), className: "bg-indigo-500 text-white py-2 px-4 rounded-lg"}, 'Click me'), _('button', {onclick: () => countSeveral.add(1), className: "bg-indigo-500 text-white py-2 px-4 rounded-lg"}, 'Click me'), _('button', {onclick: () => countSeveral.add(1), className: "bg-indigo-500 text-white py-2 px-4 rounded-lg"}, 'Click me')),_('p', {className: "text-xl font-extrabold text-gray-500 text-center mt-4"},'You clicked ', _('span', {className: "text-indigo-500"}, countSeveral), ' times' ),// Render lists _('h2', 'Render lists'),_('p', 'You can render lists of elements using an array of Ophose objects. They need to be put as children of an element though as each Ophose object is a node of the virtual DOM.'),_('p', 'This can be useful when you want to render a list of elements or a list of components.'), new CodeBlock({lines: [ "render() {", " let fruits = [", " {name: 'Apple', inStock: true},", " {name: 'Banana', inStock: false},", " {name: 'Orange', inStock: true}", " ];", "", " return _('ul',", " fruits.map(fruit => _('li',", " _('span', fruit.name),", " _('span', fruit.inStock ? 'In stock' : 'Out of stock')"," ))"," );","}",]}),_('ul', _('li', 'Apple', _('span', {className: "text-sm text-green-500 ml-2"}, 'In stock')), _('li', 'Banana', _('span', {className: "text-sm text-red-500 ml-2"}, 'Out of stock')), _('li', 'Orange', _('span', {className: "text-sm text-green-500 ml-2"}, 'In stock'))),_('p', 'In the previous example, we created a list of fruits. We used the ', _('b', 'map'), ' method to create a list of Ophose objects. We then passed this list to the ', _('b', 'ul'), ' element.'),_('p', 'What if you want to render a dynamic list? You can use the ', _('b', 'dyn'), ' function to render live objects:'),new CodeBlock({lines: ["render() {"," let todos = live([]);"," let todo = live('');",""," return _('div',"," _('input', {oninput: e => todo.set(e.target.value)}),", " _('button', {onclick: () => todos.add(todo.value)}, 'Add'),", " dyn(todos, (list) => _('ul',", " list.map(todo => _('li', todo))"," ))"," );","}",]}),_('div', {className: "grid grid-cols-2 gap-4"},_('input', {oninput: e => todo.set(e.target.value), className: "border rounded-lg p-2"}), _('button', {onclick: () => todos.add(todo.value), className: "bg-indigo-500 text-white py-2 px-4 rounded-lg"}, 'Add') ), dyn(todos, (list) => _('ul', {className: "mt-4"}, list.map(todo => _('li', todo)))),// Events _('h2', 'Events'),_('p', 'You can listen to events on elements using the ', _('b', 'on[event]'), ' properties as defined in the HTML specification. For example, to listen to a click event, you can use ', _('b', 'onclick'), ' in the properties of the element.'),_('p', 'The event takes a callback function as a value. This function will be called when the event is triggered with the event object as a parameter.'),_('p', 'You can also pass arguments to the callback function by using an arrow function:'), new CodeBlock({lines: [ "render() {", " return _('button', {onclick: () => alert('Clicked')}, 'Click me');","}",]}),_('button', {onclick: () => alert('Clicked'), className: "bg-indigo-500 text-white py-2 px-4 rounded-lg mb-4"}, 'Click me'),_('p', 'In the previous example, we created a button that alerts a message when clicked. The ', _('b', 'alert'), ' function is called when the button is clicked.'),// Create pages _('h2', 'Create pages'),_('p', 'A page is a special component that represents a full page of your application. This is what the user sees when they visit a URL like a normal website.'),_('p', 'All of your pages are located in the ', _('b', 'pages'), ' folder of your project. Each page is a component that extends the ', _('b', 'Ophose.Page'), ' class.'),_('p', '2 pages are mandatory in your project:'), _('ul', _('li', _('b', 'index.js'), ' page: the first page that the user sees when they visit your website.'), _('li', _('b', 'error.js'), ' page: the page that is displayed when the user visits a page that does not exist.')),_('p', 'To create a page, you can simply return a component in the ', _('b', 'render'), ' method as you would do in a normal component:'),new CodeBlock({lines: ["// pages/about.js","class MyPage extends Ophose.Page {"," render() {"," return _('div',"," _('h1', 'About us'),"," _('p', 'We are a team of developers that love to create web applications.')"," );"," }","}","","oshare(MyPage);" ]}),this.browser("about",_('div',_('h1', {className: "font-extrabold text-xl"}, 'About us'),_('p', 'We are a team of developers that love to create web applications.')) ),_('p', 'In the previous example, we created a page that displays information about the team. We then shared this page using the ', _('b', 'oshare'), ' function to make it accessible by the user.'),_('p', 'The routing system of Ophose will automatically display this page when the user visits the ', _('b', '/about'), ' URL. But you can also create dynamic routes placing the page in a folder with name starting with an underscore like ', _('b', '/user/_id/index.js'), ' to create a dynamic route that will pass the ', _('b', 'id'), ' parameter to the page.'),new CodeBlock({lines: ["// pages/user/_id/index.js","class UserPage extends Ophose.Page {"," render() {"," return _('div',"," _('h1', 'User profile'),"," _('p', 'This is the profile of user ' + this.urlQueries.query.id)"," );"," }","}","","oshare(UserPage);" ]}),this.browser("user/123",_('div',_('h1', {className: "font-extrabold text-xl"}, 'User profile'),_('p', 'This is the profile of user 123')) ),// Logic and data _('h2', 'Logic and data with Environments'),_('p', 'Environments are used to handle the logic and data of your application in server-side code with PHP. An environment is a small piece of code that can be used to interact with the server, the database and other environments.'),_('p', 'Let\'s say you want to create a forum application. You will need several environments such as a ', _('b', 'Database'), ' environment to interact with the database, a ', _('b', 'User'), ' environment to handle the users, a ', _('b', 'Post'), ' environment to handle the posts, etc.'),_('p', 'Hopefully, Ophose provides a way to create environments easily and you can even download environments from the Ophose website to use them in your application.'),_('p', 'To create an environment, you can simply return a PHP ', _('b', 'Env'), ' object in the ', _('b', 'env.php'), ' file of the environment folder:'),new CodeBlock({lang: 'php', lines: ["endpoint('/posts', function() {"," return Response::json(Post::all());"," });"," }","};" ]}),_('p', 'In the previous example, we created a ', _('b', 'MyEnv'), ' environment that returns all the posts of the forum when the ', _('b', '/posts'), ' endpoint is called.'),_('p', 'The ', _('b', 'endpoint'), ' method is used to create an endpoint and is called in the ', _('b', 'endpoints'), ' method of the environment. This function takes a URL and a callback function as parameters by default.'),_('p', 'The ', _('b', 'endpoints'), ' method is called automatically by Ophose when the environment is used in HTTP requests. It is useful to create conditional routes for example if the user is authenticated or not.'),_('p', 'You can also use the ', _('b', 'Request'), ' and ', _('b', 'Response'), ' classes to handle the requests and responses. The ', _('b', 'Request'), ' class is used to get the request data and the ', _('b', 'Response'), ' class is used to send the response data.'),this.browser("api/myenv/posts",new CodeBlock({lang: 'json', lines: ["["," {"," \"id\": 1,"," \"title\": \"Hello World\","," \"content\": \"This is the first post.\""," },"," {"," \"id\": 2,"," \"title\": \"Second Post\","," \"content\": \"This is the second post.\""," }","]" ]})),// Conclusion _('h2', 'Conclusion'),_('p', 'In this tutorial, you learned how to create components, pass properties, make them interactive, render lists, listen to events, create pages, and handle logic and data with environments. This is the basic knowledge you need to start building your web applications with Ophose.'),_('p', 'Of course there are many other features in Ophose that you can use to build more complex applications and to make your development process easier. You can find more tutorials and documentation on the Ophose website.'));} }oimpc('misc/xpl/ExplainContent');class TutoInstallation extends ExplainContent {constructor() {super({title: "Installation",description: "Learn how to install Ophose on your computer or server to deploy your web applications.",identifier: 'installation' });} render() {return _('div',// Requirements _('h2', "Requirements"),_('p', "As this framework has been designed to require the least amount of dependencies, you will only need an Apache PHP server to run your applications."),_('p', 'There are then no more required Ophose except for the PHP server, which are:'),_('ul', {className: "font-semibold"},_('li', "PHP 8.0 or higher"),_('li', "PDO (for database connection)"),_('li', "CURL"),_('li', "ZIP")),_('p', 'If you\'re on Windows, you can use WAMP, XAMPP or any other PHP server and edit the configuration file to enable the required extensions.'),_('p', 'If you\'re on Linux, you can install PHP with the following command:'),new CodeBlock({lines: ["sudo apt-get install php php-pdo php-curl php-zip" ]}),_('p', 'If you\'re on MacOS, you can install PHP with Homebrew:'),new CodeBlock({lines: ["brew install php php-pdo php-curl php-zip" ]}),// Installation _('h2', "Installation"),_('p', 'To install Ophose, simply download the latest version of the framework from the ', _('a', {href: "https://github.com/ah-4/ophose-release"}, "github repository"), '.'),_('p', 'Then, extract the archive in the directory of your choice, and you\'re ready to go. As this is a git repository, you can easily version your project and update as there already is a ', _('b', "gitignore"), ' file.'),_('p', 'You\'re now ready to go for it!'),// Installating resources _('h2', "Installing resources"),_('p', 'You may not want to start your project from scratch. That\'s why Ophose provides you with a set of resources to start your project.'),_('p', 'To install the resources, specify the resources you want to install in the ', _('b', "project.oconf"), ' file, and run the following command and ', _('b', "dependencies"), ' section then run:'),new CodeBlock({lines: ["php ocl ophose install" ]}),_('p', 'This will automatically install the resources in the right directories of your project, and you\'ll be able to use them in your project.'),_('p', 'You\'ll see later in the documentation how to use the resources in your project and even how to create your own resources.'),// Updating Ophose _('h2', "Updating Ophose"),_('p', 'To update Ophose, you can simply run the following command:'),new CodeBlock({lines: ["php ocl ophose update" ]}),_('p', 'This will automatically update the framework to the latest version and you\'ll be able to use the new features and bug fixes of the framework.'),// Configuration _('h2', "Configuration"),_('p', 'To configure your project, you can create a ', _('b', "project.oconf"), ' file at the root of your project. This file will contain the configuration of your project, such as the name of your project, the API key, the dependencies, etc.'),_('p', 'Here is an example of a configuration file:'), new CodeBlock({lines: [ '{', ' "name": "Your Ophose Project",', ' "production_mode": false,', ' "api_key": "YOUR_API_KEY",', ' "secure_key": "YOUR_SECURE_KEY",', ' "url": "http://localhost/",', ' "project_id": "your_project_id",', ' "build": {', ' "redirect": {', ' "force_https_redirect": false', ' }', ' },', ' "dependencies": {', ' "ah4/database": "latest",', ' "ah4/storage": "latest",', ' "ah4/starter": "latest",', ' },', ' "ophose": {', ' "version": "1.0.0_d_pre1_d"', ' }', '}' ]}),_('p', 'This configuration can be overridden by creating a ', _('b', "project.oconf.local"), ' file, which will contain the local configuration of your project, such as the secure key, the API key, etc.'),_('p', {className: "alert"}, "Note: The default configuration keys will be recursively merged with the local configuration keys, so you don't have to specify all the keys in the local configuration file."),);} }oimpc('misc/xpl/ExplainContent');class TutoComponent extends ExplainContent {constructor() {super({title: "Your first component",description: "Learn how to create and use components in Ophose.",identifier: 'component' });} render() {return _('div',// Understanding logic of UI _('h2', "Understanding logic of UI"),_('p', "In Ophose, the user interface is built with components in JavaScript. A component is a class that extends the ", _('b', "Ophose.Component"), " class and that has a ", _('b', "render"), " method that returns the HTML structure of the component in JSON. Everything that will be rendered is called an ", _('b', "Ophose object"), ". They can either be strings, components, objects..."),_('p', 'Building so, allows your web application to be loaded faster and only client-side, as the server only sends the JSON structure of the component to the client.'),_('p', 'For example, the following HTML code:'),new CodeBlock({lang: 'html', lines: ['
This is a paragraph.
','