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: ['
','

Hello, world!

','

This is a paragraph.

','
' ]}),_('p', 'Would be represented in Ophose as:'),new CodeBlock({lang: 'javascript', lines: ["return {"," _: 'div',"," children: ["," {_: 'h1', children: 'Hello, world!'},"," {_: 'p', children: 'This is a paragraph.'}"," ]","}" ]}),_('p', 'As you can see, the node name is represented by the ', _('b', "_"), ' key, and the children of the node are represented by the ', _('b', "children"), ' key. In these children you can either put a string, an array of strings, an array of Ophose objects or a single Ophose object.'),_('p', 'This is the basic structure of a component in Ophose. You can then add more keys to the object to add more properties to the node, such as ', _('b', "className"), ', ', _('b', "id"), ', ', _('b', "style"), ', ', _('b', "onclick"), '...'),_('p', 'But writing this JSON structure can be a bit tedious. That\'s why Ophose provides you with a set of functions to create these objects easily. For example, the previous example would be written as:'),new CodeBlock({lang: 'javascript', lines: ["return _('div',"," _('h1', 'Hello, world!'),"," _('p', 'This is a paragraph.')",")" ]}),_('p', 'Much easier, right?'),// Adding properties and attributes _('h2', "Adding properties and attributes"),_('p', 'In classic HTML, you can add properties and attributes to a node by adding them in the tag. For example:'),new CodeBlock({lang: 'html', lines: ['' ]}),_('p', 'In Ophose, you can add properties and attributes to a node by adding them as keys in the object. For example:'),new CodeBlock({lang: 'javascript', lines: ["return {"," _: 'input',", " type: 'text',", " placeholder: 'Enter your name'","}" ]}),_('p', 'This will render the same HTML code as the previous one. But you can also write it as:'),new CodeBlock({lang: 'javascript', lines: ["return _('input', {type: 'text', placeholder: 'Enter your name'})" ]}),_('p', 'Here, the second argument of the function is an object that contains the properties and attributes of the node. This is the same for all the nodes in Ophose.'),_('p', 'One of the major advantages of Ophose is that you can directly put JavaScript expressions in the attributes. For example:'),new CodeBlock({lang: 'javascript', lines: ["let text = 'Click me';", "let message = 'Hello, world!';", "return _('button', {onclick: () => alert(message)}, text)" ]}),_('p', 'This will create a button that, when clicked, will show an alert with the message "Hello, world!". You also saw that you can put a JavaScript expression in the children of the node or in the event listeners.'),// Creating a component _('h2', "Creating a component"),_('p', 'To create a component in Ophose, you need to create a class that extends the ', _('b', "Ophose.Component"), ' class and that has a ', _('b', "render"), ' method that returns the Ophose object of the component.'),_('p', 'For example, let\'s create a simple component that displays a button that, when clicked, shows an alert with a message:'),new CodeBlock({lang: 'javascript', lines: ["class MyButton extends Ophose.Component {"," constructor(props) {"," super(props);"," }",""," render() {"," return _('button', {onclick: () => alert('Hello, world!')}, 'Click me');"," }","}" ]}),_('p', 'This component can then be used in another component or in a page by creating an instance of the class:'),new CodeBlock({lang: 'javascript', lines: ["class MyComponent extends Ophose.Component {"," constructor(props) {"," super(props);"," }",""," render() {"," return _('div',"," _('h1', 'My component'),"," new MyButton()"," )"," }","}" ]}),_('p', 'One of the advantages of Ophose is that you can directly put a component in the children of a node, and it will be rendered as expected. You can also call methods of the child component, pass props to it, and even listen to events of the component.'),_('p', 'This is the basic structure of a component in Ophose. You can then add more keys to the object to add more properties to the node, such as ', _('b', "className"), ', ', _('b', "id"), ', ', _('b', "style"), ', ', _('b', "onclick"), '...'),// Adding styles _('h2', "Adding styles"),_('p', 'In Ophose, you can add styles to a component with the ', _('b', "style"), ' method of the it. This method should return a CSS string that will be added to the component. For example:'),new CodeBlock({lang: 'javascript', lines: ["class MyComponent extends Ophose.Component {"," constructor(props) {"," super(props);"," }",""," style() {"," return `%self {"," background-color: #f0f0f0;"," padding: 1rem;"," border-radius: 0.5rem;"," }`"," }",""," render() {"," return _('div',"," _('h1', 'My component'),"," new MyButton()"," )"," }","}" ]}),_('p', 'This will add the styles to the component and will render the component with the styles applied. You can also add styles to the children of the component by adding the ', _('b', "style"), ' method to the children of the component.'),_('p', {className: 'alert'}, 'Note that the styles added with the ', _('b', "style"), ' method are scoped to the component with the ', _('b', "%self"), ' selector. This means that the styles will only be applied to the component and its children, and not to the whole page. This is useful to avoid conflicts between the styles of different components.'),// Passing props and children _('h2', "Passing props and children"),_('p', 'In Ophose, you can pass props to a component by passing an object to the constructor of the component. For example:'),new CodeBlock({lang: 'javascript', lines: ["class MyComponent extends Ophose.Component {"," constructor(props) {"," super(props);"," }",""," render() {"," return _('div',"," _('h1', this.props.title),"," _('p', this.props.description)"," )"," }","}","","new MyComponent({title: 'Hello, world!', description: 'This is a paragraph.'})" ]}),_('p', 'This will render a div with an h1 and a p inside, with the title and description passed as props. You can then use these props in the render method of the component.'),_('p', 'You can also pass children to a component by passing them in the children key of the object. For example:'),new CodeBlock({lang: 'javascript', lines: ["class Container extends Ophose.Component {"," constructor(props) {"," super(props);"," }",""," render() {"," return _('div',"," _('h1', this.props.title)"," this.props.children"," )"," }",]}),_('p', 'But if you had understood the previous sections, you may guess that ', _('b', "this.props.children"), ' is just a property like any other.'),// Rendering lists _('h2', "Rendering lists"),_('p', 'In Ophose, you can render lists by passing an array of Ophose objects to the children key of a node. For example:'),new CodeBlock({lang: 'javascript', lines: ["let items = ['Item 1', 'Item 2', 'Item 3'];","return _('ul', items.map((item) => _('li', item)))" ]}),_('ul', {className: "font-semibold"},_('li', "Item 1"),_('li', "Item 2"),_('li', "Item 3")),_('p', 'This will render an unordered list with three list items, each containing one of the items in the array. You can also pass an array of components to the children key, and they will be rendered as expected.'),_('p', {className: 'alert'}, 'Note that you can\'t directly render lists as Ophose needs a parent node to render the children. That\'s why you need to put the list in a parent node.'),// Importing components _('h2', "Importing components"),_('p', 'In Ophose, you can import components from other files by using the ', _('b', "oimpc"), ' function. All your components must be placed in the ', _('b', "components"), ' folder of your project. Then you can import them in your component by using the ', _('b', "oimpc"), ' function. For example:'),new CodeBlock({lines: ["// in /components/MyButton.js","class MyButton extends Ophose.Component {"," constructor(props) {"," super(props);"," }",""," render() {"," return _('button', {onclick: () => alert('Hello, world!')}, 'Click me');"," }","}",]}),new CodeBlock({lines: ["// in /components/MyComponent.js","oimpc('MyButton');","","class MyComponent extends Ophose.Component {"," constructor(props) {"," super(props);"," }",""," render() {"," return _('div',"," _('h1', 'My component'),"," new MyButton()"," )"," }","}",]}),_('p', 'This will import the ', _('b', "MyButton"), ' component in the ', _('b', "MyComponent"), ' component and render it as expected.'),_('p', {className: "alert"}, 'It is really important to specify the path of the component in the ', _('b', "oimpc"), ' function. If you don\'t, Ophose will not be able to find the component and will throw an error or may cause some errors in the production build.'),// Lifecycle methods _('h2', "Lifecycle methods"),_('p', 'In Ophose, you can use lifecycle methods to add interactivity to your components. The lifecycle methods are methods that are called at different stages of the component\'s life. The main lifecycle methods are:'),_('ul', {className: "font-semibold"},_('li', _('b', "constructor(props)"), ': The constructor of the component, called when the component is created.'),_('li', _('b', "render"), ': The render method of the component, called when the component is rendered.'),_('li', _('b', "onPlace(node)"), ': The onPlace method of the component, called when the component is placed in the DOM.'),_('li', _('b', "onRemove(node)"), ': The onRemove method of the component, called when the component is removed from the DOM.'),),// Conclusion _('h2', "Conclusion"),_('p', 'This is the basic structure of a component in Ophose. You can then add more keys to the object to add more properties to the node, such as ', _('b', "className"), ', ', _('b', "id"), ', ', _('b', "style"), ', ', _('b', "onclick"), '...'),_('p', 'You can also add more methods to the class to add more interactivity to the component, such as event listeners, data fetching, state management...'),_('p', 'You can also create more complex components by nesting components in other components, passing props to them, and listening to events of the child components.'),);} }oimpc('misc/xpl/ExplainContent');class TutoLive extends ExplainContent {constructor() {super({title: "Adding interactivity",description: "Learn how to add interactivity to your components.",identifier: 'interactivity' });} render() {let count = live(0);let show = live(true);return _('div',// What is Live? _('h2', "What is Live?"),_('p', "Live is a feature of Ophose that allows you to add interactivity to your components. It is simply a tracked variable that will be updated whenever its value changes. It cas be really useful for example to load data from an API, or to update the UI when a user interacts with your component."),// How to declare a Live variable _('h2', "How to declare a Live variable"),_('p', "To declare a Live variable, you can simply use the ", _('b', "live"), " function. This function takes a single argument, which is the initial value of the Live variable. It is as simple as that."),new CodeBlock({lines: ["render() {"," let count = live(0);","}" ]}),// How to use a Live variable _('h2', "How to use a Live variable"),_('p', 'There are 2 main ways you may want to use a Live variable: '),_('ul', {className: "font-semibold"},_('li', "To read the value of the Live variable"),_('li', "Update the value of the Live variable")),_('p', "To read the value of a Live variable, you can call the attribute ", _('b', "value"), " on the Live variable. This will return the current value of the Live variable."),_('p', 'If you simply want to display the value of the Live variable as a node, you can simply use it as an Ophose object. The value will be automatically updated when the Live variable changes.'),new CodeBlock({lines: ["render() {"," let count = live(0);"," return _('div', count);","}" ]}),_('p', "To update the value of a Live variable, you can call the method ", _('b', "set"), " on the Live variable. This method takes a single argument, which is the new value of the Live variable. The value will be updated and all the components that use this Live variable will be updated."),_('p', {className: "alert"}, 'You may also want to use the method ', _('b', "update"), ' on the Live variable. This method takes a callback as argument, which will be called with the current value of the Live variable and should return the new value. This can be useful if you want to update the value of the Live variable based on the current value.'),_('p', "There are also some helper methods that can be useful: "),_('ul', {className: "font-semibold"},_('li', _('b', "add"), ": Adds a value to the current value or array"),_('li', _('b', "remove"), ": Removes a value from the current value"),_('li', _('b', "toggle"), ": Toggles the value between 2 values (true/false)")),new CodeBlock({lines: ["render() {"," let count = live(0);"," return _('div',"," _('button', {onclick: () => count.add(1)}, 'Increment'),"," count"," );","}" ]}),_('div', {className: "grid grid-cols-2 items-center text-center"},_('button', {onclick: () => count.add(1), className: "py-2 px-4 bg-indigo-500 text-white rounded-lg"}, 'Increment'),count ),// Dynamic rendering _('h2', "Dynamic rendering"),_('p', "Live variables can also be used to dynamically render components. This can be useful if you want to show or hide a component based on a condition for example or to render a list of components based on an array."),_('p', 'To do so you can use the ', _('b', "dyn"), ' function. This function takes live variables as arguments and a callback as last argument. The callback will be called whenever one of the live variables changes and should return the Ophose object to render.'),_('p', {className: "alert"}, 'The callback is called with values of the live variables as arguments in the same order as they are passed to the ', _('b', "dyn"), ' function.'),new CodeBlock({lines: ["render() {"," let show = live(true);"," return _('div',"," _('button', {onclick: () => show.toggle(), 'Toggle'),"," dyn(show, (vShow) => vShow && _('div', 'Hello world'))"," );","}" ]}),_('p', 'Or in newer versions of Ophose, you can use the ', _('b', "_if"), ' method to conditionally render a component.'),new CodeBlock({lines: ["render() {"," let show = live(true);"," return _('div',"," _('button', {onclick: () => show.toggle(), 'Toggle'),"," show._if(_('div', 'Hello world'))"," );","}" ]}),_('div', {className: "grid grid-cols-2 items-center text-center"},_('button',{onclick: () => show.toggle(), className: "py-2 px-4 bg-indigo-500 text-white rounded-lg"}, 'Toggle'),dyn(show, (vShow) => vShow && _('div', 'Hello world'))),_('p', "In this example, we have a button that toggles the value of the Live variable ", _('b', "show"), ". The component will be rendered only if the value of the Live variable is true."),// Use live variables in expressions _('h2', "Use live variables in expressions"),_('p', "At the moment, you can only use live variables as Ophose objects. For example if you try to use a live variable in an expression, it will not work. This is a limitation of the current version of Ophose and will be fixed in the future."),_('p', "For example, the following code will not work:"),new CodeBlock({lines: ["render() {"," let count = live(0);"," return _('div', count + 1); // This will not work","}" ]}),_('p', "To make it work, you can use the ", _('b', "dyn"), " function as explained above."),new CodeBlock({lines: ["render() {"," let count = live(0);"," return _('div', dyn(count, (vCount) => vCount + 1));","}" ]}),_('p', "This will work as expected."),// Watch live variables _('h2', "Watch lives"),_('p', "You can also watch live variables to execute a callback whenever the value of the live variable changes. This can be useful if you want to load data from an API when the value of a live variable changes for example."),_('p', "To do so, you can use the ", _('b', "watch"), " function. This function works like the ", _('b', "dyn"), " function: it takes live variables as arguments and a callback as last argument. The callback will be called whenever one of the live variables changes and when it is initially called."),_('p', {className: "alert"}, 'The callback is called with values of the live variables as arguments in the same order as they are passed to the ', _('b', "watch"), ' function.'),new CodeBlock({lines: ["render() {"," let count = live(0);"," watch(count, (vCount) => console.log(vCount));"," return _('div',"," _('button', {onclick: () => count.add(1)}, 'Increment'),"," count"," );","}" ]}),_('p', "You may also want to associate a live variable to an element. To do so, you can use the ", _('b', "watch"), " property. This property takes a live variable as value. The element will be updated whenever the value of the live variable changes and the live variable will be updated whenever the element changes."),// Live rules _('h2', "Live rules"),_('p', "Live rules are a way to add a rule to a live variable. A rule is a function that takes the current value of the live variable and returns the new value. This can be useful to fastly make sure that the value of a live variable is always in a certain range for example."),_('p', "To add a rule to a live variable, you can use the ", _('b', "rule"), " method. This method takes a callback as argument, which is the rule to apply to the live variable. The callback will be called with the current value and the previous value of the live variable and should return the new value."),_('p', 'There are some helper methods that can be useful: '),_('ul', {className: "font-semibold"},_('li', _('b', "min"), ": Adds a rule to the live variable to make sure that the value is at least a certain value"),_('li', _('b', "max"), ": Adds a rule to the live variable to make sure that the value is at most a certain value")),new CodeBlock({lines: ["render() {"," let count = live(0).min(0).max(10);"," return _('div',"," _('button', {onclick: () => count.add(1)}, 'Increment'),"," count"," );","}" ]}),_('p', "Note that you can also specify other live variables in the rules which will automatically the live variable rules depending on them."),// Conclusion _('h2', "Conclusion"),_('p', "Adding interactivity to your components is really easy with Live variables. You can use them to read and update values, to dynamically render components and to use them in expressions. This is a really powerful feature that can help you build more interactive and dynamic components."),);} }oimpc('misc/xpl/ExplainContent');class TutoPages extends ExplainContent {constructor() {super({title: "Create pages",description: "See how the routing system works in Ophose to create pages.",identifier: 'pages' });} render() {return _('div',// What is a page? _('h2', "What is a page?"),_('p', "A page is a component that will be used to display the content of a specific URL. It is a component that will be used to define the content of a specific URL, and that will be used to display the content of your application."),_('p', "A page extends the ", _('b', "Ophose.Page"), " class that extends the ", _('b', "Ophose.Component"), " class. It is a component like any other, but it will be used in a special way. It will be used to display the content of a specific URL and will be the component that will be displayed when the user navigates to this URL."),// How to create a page _('h2', "How to create a page"),_('p', "To create a page, you will need to create the file of your page at ", _('b', "pages/PageName.js"), ". This file will contain the definition of your page component and must extend the ", _('b', "Ophose.Page"), " class."),_('p', {className: "alert"}, "Note that you must export your page component with the ", _('b', "oshare"), " function and the ", _('b', "PageName"), " class name as argument."),new CodeBlock({lines: ["class PageName extends Ophose.Page {"," constructor(urlQueries) {"," super(urlQueries);"," }","}","oshare(PageName);" ]}),_('p', "You can then define the content of your page in the ", _('b', "render"), " method of your page component. This method will return the content of your page, and will be used to display the content of your page."),_('p', "For example, if you want to create an About page, you can create the file ", _('b', "pages/about.js"), " and define the content of your page in the ", _('b', "render"), " method of your page component."),new CodeBlock({lines: ["// In pages/about.js","render() {"," return _('div',"," _('h1', 'About'),"," _('p', 'This is the about page.')"," );","}" ]}),_('p', "You can then navigate to this page by going to the URL ", _('b', "/about"), "."),this.browser("about", [_('h1', {className: "text-2xl font-bold"}, "About"),_('p', "This is the about page.")]),// URL queries _('h2', "URL queries"),_('p', "You may want to use URL queries in your pages to get data from the URL. For example, if you want to create a page that displays a user profile, you may want to get the user ID from the URL to display the user profile."),_('p', "To get URL queries in your page, you can simply use the ", _('b', "this.urlQueries"), " attribute. This attribute will contain the URL queries of the current URL, and you can use it to get the data from the URL."),_('p', "Note that there are 2 types of URL queries: "),_('ul', {className: "font-semibold"},_('li', _('b', "query"), ": Contains the query parameters of the URL"),_('li', _('b', "get"), ": Contains the GET parameters of the URL")),_('p', "For example, if you want to get the user ID from the URL ", _('b', "/profile/john-doe"), ", you will first need to create the file ", _('b', "pages/profile/_userId/index.js"), " and define the content of your page in the ", _('b', "render"), " method of your page component."),_('p', {className: "alert"}, "Note that you must create a directory with the name of the URL query parameter starting with an underscore ", _('b', "_userId"), " and create an ", _('b', "index.js"), " file in this directory to define the content of your page."),new CodeBlock({lines: ["// In pages/profile/_userId/index.js","render() {"," let userId = this.urlQueries.query.userId;"," return _('div',"," _('h1', 'User profile'),"," _('p', 'User ID: ' + userId)"," );","}" ]}),_('p', "You can then navigate to this page by going to the URL ", _('b', "/profile/john-doe"), "."),this.browser("profile/john-doe", [_('h1', {className: "text-2xl font-bold"}, "User profile"),_('p', "User ID: john-doe")]),_('p', "You can also use the ", _('b', "get"), " attribute to get the GET parameters of the URL. For example, if you want to get the page number from the URL ", _('b', "/blog?page=2"), ", you can use the ", _('b', "get"), " attribute to get the page number."),new CodeBlock({lines: ["// In pages/blog/index.js","render() {"," let page = this.urlQueries.get.page;"," return _('div',"," _('h1', 'Blog'),"," _('p', 'Page number: ' + page)"," );","}" ]}),_('p', "You can then navigate to this page by going to the URL ", _('b', "/blog?page=2"), "."),this.browser("blog?page=2", [_('h1', {className: "text-2xl font-bold"}, "Blog"),_('p', "Page number: 2")]),// Redirect _('h2', "Redirect"),_('p', "You may want to redirect the user to another page in your page. For example, if the user is not logged in, you may want to redirect the user to the login page."),_('p', "To redirect the user to another page, you can use the ", _('b', "this.redirect"), " method. This method takes a single argument, which is the URL of the page you want to redirect the user to."),new CodeBlock({lines: ["// In pages/dashboard/index.js","render() {"," if(!isLoggedIn) {"," this.redirect('/login');"," }"," return _('div',"," _('h1', 'Dashboard'),"," _('p', 'Welcome to the dashboard.')"," );","}" ]}),_('p', "You can then navigate to this page by going to the URL ", _('b', "/dashboard"), "."),// 404 page _('h2', "404 page"),_('p', "What if the user navigates to a page that does not exist? You may want to display a 404 page to let the user know that the page does not exist."),_('p', "To create a 404 page, you will need to create the file ", _('b', "pages/error.js"), " and define the content of your 404 page in the ", _('b', "render"), " method of your page component as you would do for any other page."),new CodeBlock({lines: ["// In pages/error.js","render() {"," return _('div',"," _('h1', '404'),"," _('p', 'Page not found.')"," );","}" ]}),_('p', "You can then navigate to this page by going to a URL that does not exist, for example ", _('b', "/404"), "."),_('p', "If the user navigates to a page that does not exist, the 404 page will be displayed."),this.browser("page-that-does-not-exist", [_('h1', {className: "text-2xl font-bold"}, "404"),_('p', "Page not found.")]),// Conclusion _('h2', "Conclusion"),_('p', "You have learned how to create pages in Ophose. You have seen how to create a page, how to use URL queries, how to redirect the user to another page, and how to create a 404 page."),_('p', "You can now create pages for your application and display the content of your pages based on the URL."));} }oimpc('misc/xpl/ExplainContent');class TutoEnvironment extends ExplainContent {constructor() {super({title: "Logic and data",description: "Add logic and data to your application with Ophose.",identifier: 'environment' });} render() {return _('div',// What is an environment? _('h2', "What is an environment?"),_('p', "You can see an environment as a service in your application. For example, you can have an environment to manage the user authentication, an environment to manage the data from an API, or an environment to manage the database."),_('p', "Let's take the example of an environment to manage the user authentication. This environment will have methods to log in, log out, check if the user is authenticated, and get the user information. But it will also need"),_('ul',_('li',_('b', 'Databse environment:')," to store the user information in the database" ),_('li',_('b', 'Storage environment:')," to store files like the user profile picture" ),),_('p', 'And once you have all these environments, you can use them in server-side to handle your data and logic. And create endpoints to interact with your application in client-side in a secure way.'),// How to create an environment _('h2', "How to create an environment"),_('p', "To create an environment, you will need to create the folder of your environment at ", _('b', "env/EnvironmentName/"), ". This folder will contain the definition of your environment class and all the methods to manage your data and logic."),_('p', "Then you will either need to create a file:"),_('ul',_('li',_('b', 'env.php:')," if you need to create endpoints to interact with your application in client-side or commands" ),_('li',_('b', 'env.oconf:')," if you need to configure your environment" ),),_('p', {className: "alert"}, "If one of these files is missing, the environment will not be loaded."),_('p', "Let's create an environment that will simply sends random messages. We will create the folder ", _('b', "env/RandomMessage/"), " and create the file ", _('b', "env/RandomMessage/src/Messenger.php"), " with the following content:"),new CodeBlock({lang: 'php', lines: ["$messages[array_rand(self::$messages)];"," }","","};" ]}),_('p', "Note that to load your classes, you will need to create your files in the ", _('b', "src/"), " folder of your environment and specify the namespace of your environment at the beginning of the file as ", _('b', "namespace YourEnvironment;"), ". Note that if your class is in a subfolder, you will need to specify the namespace as ", _('b', "namespace YourEnvironment\\Subfolder;"), "."),_('p', "You can now also preload your classes by adding the preload key in the ", _('b', "env.oconf"), " file of your environment if you want them to be loaded whenever a request is made to your application:"),new CodeBlock({lang: 'json', lines: ["{"," \"preload\": ["," \"file/path/relative/to/src.php\""," ]","}" ]}),_('p', "We now need to create the file ", _('b', "env/RandomMessage/env.php"), " to create an endpoint to get the message. We will use the ", _('b', "Env"), " class to create the endpoint:"),new CodeBlock({lang: 'php', lines: ["endpoint('message', function() {"," return Response::json(["," 'message' => Messenger::getMessage()"," ]);"," });"," }","","};" ]}),_('p', "You can now fetch the message by calling the endpoint ", _('b', "/api/randommessage/message"), " in your application."),this.browser("api/randommessage/message", new CodeBlock({lang: 'json', lines: ["{"," \"message\": \"Good evening!\"","}" ]})),_('p', "You can now use this environment in your application to get random messages."),// Getting dynamic URL queries _('h2', "Getting dynamic URL queries"),_('p', "You may want to use dynamic URL queries in your environment to get data from the URL. For example, let's take our previous example of random messages. You may want to fetch a specific message by its index."),_('p', 'First, let\'s add a method to get the message by its index in the file ', _('b', "env/RandomMessage/src/Messenger.php"), ':'),new CodeBlock({lang: 'php', lines: [" public function getMessageAt($index) {", " return self::$messages[$index];"," }",]}),_('p', "Then let's create another endpoint in the file ", _('b', "env/RandomMessage/env.php"), " to get the message by its index:"),new CodeBlock({lang: 'php', lines: ["endpoint('message', function() {"," return Response::json(["," 'message' => Messenger::getMessage()"," ]);"," });",""," $this->endpoint('message/_id', function($index) {"," return Response::json(["," 'message' => Messenger::getMessageAt($index)"," ]);"," });"," }","","};" ]}),_('p', "You can now fetch the message by calling the endpoint ", _('b', "/api/randommessage/message/2"), " in your application."),this.browser("api/randommessage/message/2", new CodeBlock({lang: 'json', lines: ["{"," \"message\": \"Good morning!\"","}" ]})),// Request and response _('h2', "Requests and responses"),_('p', "You may want to get data from the request in your environment, or send data in the response as we previously did with the message. You can use the ", _('b', "Request"), " and ", _('b', "Response"), " classes to get the request data and send the response data."),_('p', 'Here are some methods you can use with the ', _('b', "Request"), ' class:'),_('ul',_('li',_('b', 'query($key, $default = null):')," Get a GET parameter from the request" ),_('li',_('b', 'post($key, $default = null):')," Get a POST parameter from the request" ),_('li',_('b', 'file($key):')," Get a file from the request" ),_('li',_('b', 'json($default = []):')," Get the JSON body from the request" ),),_('p', 'And here are some methods you can use with the ', _('b', "Response"), ' class:'),_('ul',_('li',_('b', 'json($data, int $status = 200):')," Send a JSON response" ),_('li',_('b', 'raw($data, int $status = 200):')," Send an an unformatted response" ),_('li',_('b', 'file($path, int $status = 200):')," Send a file in the response" ),_('li',_('b', 'download($path, $filename = null, int $status = 200):')," Send a file in the response as an attachment" ),_('li',_('b', 'html($data, int $status = 200):')," Send an HTML response" )),_('p', "You can now use these classes in your environment to get data from the request and send data in the response."),// Using the environment _('h2', "Using the environment"),_('p', "If you want to use the environment in another environment, you can simply import the environment by using the ", _('b', "use"), " keyword."),_('p', "For example, if you want to use the ", _('b', "Messenger"), " class in another environment, you can simply import the class at the beginning of the file:"),new CodeBlock({lang: 'php', lines: [" {"," users = response;","});" ]}),_('p', "You can then use the ", _('b', "users"), " to fetch the data from your back-end."),// Sending data to the back-end _('h2', "Sending data to the back-end"),_('p', "To send data to the back-end, you can use the ", _('b', "oenv"), " function with the options of the request."),_('p', "For example, if you want to send a POST request to your back-end to create a new user, you can use the following code:"),new CodeBlock({lines: ["let newUser = {"," name: 'John Doe',"," email: 'username@example.com'","};","oenv('/auth/create_user', newUser).then(response => {"," console.log('User created:', response);","});" ]}),_('p', "You can then use the ", _('b', "newUser"), " to send the data to your back-end."),// Conclusion _('h2', "Conclusion"),_('p', "You have now learned how to create a fullstack application with Ophose. You can now create the front-end and the back-end of your application and make the bridge between them. You're now ready to create your own fullstack applications with Ophose!"),);} }class Blocker extends Ophose.Component {constructor(props) {super(props);} style() {return /* css */` %self { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); backdrop-filter: blur(5px); z-index: 100; } %self .blocker { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 100; inset: 0; } %self .child { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 110; } %self .blocker_close_button { position: absolute; top: 0; right: 0; padding: 1rem; cursor: pointer; color: white; font-size: 1rem; } ` } render() {let processClose = () => {this.props.onClose && this.props.onClose();this.remove();} return {_: 'div', children: [_('div', {className: 'blocker', onclick: () => {processClose()}}),_('div', {className: 'child'},_('div', {className: 'blocker_close_button', onclick: () => {processClose()}}, '✖'),this.props.children )]}} }oimpc('@/AH4/Starter/Blocker');oimpc('@/AH4/Starter/XForm');oimpc('@/AH4/Starter/XInput');class LoginModal extends Ophose.Component {constructor(props) {super(props);this.tab = live('login');this.showEmailVerification = live(false);} style() {return /* css */` %self .char_counter { text-align: right; display: block; } %self label { font-size: 0.875rem; font-weight: 500; color: #374151; margin-bottom: 0.5rem; } %self .form_group { width: 100%; } %self .errors li { color: red; font-size: 0.75rem; } ` } render() {let _login = new XForm({className: "p-8 flex flex-col gap-4 items-start w-full", onSuccess: (r) => {this.props.onClose();Auth.user().then((user) => {lives.user.set(user);});}, endpoint: 'ah4/auth/login', c: [_('p', {className: "text-gray-500"}, "Login to your account"),_('h2', {className: "text-2xl font-semibold"}, "Login"),new XInput({name: "username",label: "Email",required: true,placeholder: "Enter your email",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500" }),new XInput({name: "password",label: "Password",type: "password",required: true,placeholder: "Enter your password",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500" }),_('p', {className: "text-sm text-gray-500"}, "Don't have an account? ", _('a', {className: "text-blue-500 hover:underline cursor-pointer", onclick: () => this.tab.set('register')}, "Register now"), "!"),_('button', {type: 'submit', className: "w-full px-4 py-2 rounded-md bg-blue-500 text-white font-semibold hover:bg-blue-600 transition duration-200 ease-in-out"}, "Login")]});let _register = new XForm({className: "p-8 flex flex-col gap-4 items-start w-full", endpoint: 'ah4/auth/register', onSuccess: () => {this.showEmailVerification.set(true);}, c: [_('p', {className: "text-gray-500"}, "Register a new account"),_('h2', {className: "text-2xl font-semibold"}, "Register"),new XInput({name: "email",label: "Email",required: true,placeholder: "Enter your email",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500" }),new XInput({name: "username",label: "Username",required: true,placeholder: "Enter your username",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500" }),new XInput({name: "password",label: "Password",type: "password",required: true,placeholder: "Enter your password",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500" }),new XInput({name: "password_confirm",label: "Confirm Password",type: "password",required: true,placeholder: "Confirm your password",className: "w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500" }),_('p', {className: "text-sm text-gray-500"}, "Already have an account? ", _('a', {className: "text-blue-500 hover:underline cursor-pointer", onclick: () => this.tab.set('login')}, "Login now"), "!"),_('p', {className: "text-sm text-gray-500"}, "By clicking Register, you agree to our ", _('a', {className: "text-blue-500 hover:underline cursor-pointer"}, "Terms of Service"), " and ", _('a', {className: "text-blue-500 hover:underline cursor-pointer"}, "Privacy Policy"), "."),dyn(this.showEmailVerification, (show) => {if (!show) return;return _('p', {className: "text-sm text-green-500"}, "A verification email has been sent to your email address. Please verify your email to continue.");}),_('button', {type: 'submit', className: "w-full px-4 py-2 rounded-md bg-blue-500 text-white font-semibold hover:bg-blue-600 transition duration-200 ease-in-out"}, "Register")]});return new Blocker({onClose: this.props.onClose,c: [_('div', {className: "relative mx-auto w-[80vw] h-[90vh] bg-white grid lg:grid-cols-2 rounded-xl overflow-y-auto"},// Image _('img', {src: "/assets/img/auth/auth.jpg", className: "object-cover w-full h-[90vh] hidden lg:block"}),dyn(this.tab, (tab) => tab === 'login' ? _login : _register )) ]})} }oimpc('app/user/LoginModal');class HeaderUserButton extends Ophose.Component {constructor(props) {super(props);this.showLoginModal = live(false);this.showMenu = live(false);} render() {return _('div', {className: "relative"},dyn(lives.user, (user) => {// Logged in if (user) {return _('div', {className: "p-3 rounded-full hover:bg-gray-100 w-fit cursor-pointer", onclick: () => this.showMenu.toggle()},_('img', {className: "h-6 w-6 rounded-full object-cover", src: '/api/user/profile_picture/' + user.id, onclick: () => {console.log('User clicked');}})) } // Not logged in return _('i', {className: "bi bi-person py-2 px-3 rounded-full hover:bg-gray-100", onclick: () => this.showLoginModal.set(true)})}),dyn(this.showMenu, (show) => {if (!show) return;return _('div', {className: "absolute top-12 right-0 bg-gray-50 shadow-md rounded-md w-64 text-base z-30"},_('div', {className: "flex items-center gap-2 p-4"},_('img', {className: "h-8 w-8 rounded-full object-cover", src: '/api/user/profile_picture/' + lives.user.get().id}),_('div', {className: "flex flex-col"},_('span', {className: "text-base"}, lives.user.value.username),_('p', {className: "text-xs text-gray-500"}, 'Current balance: $0.00'),_('p', {className: "text-xs text-gray-500"}, 'Plan: Free')) ),_('hr'),_('div', {className: "flex flex-col font-semibold cursor-pointer", onclick: () => this.showMenu.set(false)},_('go', {href: "/me", className: "hover:bg-gray-100 rounded-md px-4 py-2"}, "Dashboard"),_('go', {href: "/me/settings", className: "hover:bg-gray-100 rounded-md px-4 py-2"}, "Settings"),_('a', {onclick: () => {Auth.logout();lives.user.set(null);this.showMenu.set(false);route.go('/');}, className: "hover:bg-gray-100 rounded-md px-4 py-2"}, "Logout")) )}),dyn(this.showLoginModal, (show) => {if (!show) return;return new LoginModal({onClose: () => this.showLoginModal.set(false)});}));} }oimpc('@/AH4/Starter/Link');oimpc('app/user/HeaderUserButton');class Header extends Ophose.Component {constructor(props) {super(props);this.updateHeaderClassesOnScroll = () => {let element = this.getNode();if (window.scrollY > 0) {element.classList.add('bg-white');element.classList.add('shadow-md');} else {element.classList.remove('bg-white');element.classList.remove('shadow-md');} }} onPlace(element) {window.addEventListener('scroll', this.updateHeaderClassesOnScroll);this.updateHeaderClassesOnScroll();} render() {let _responsiveMenu = () => {return _('div', {className: "lg:hidden container mx-auto flex items-center gap-4"},// Logo and title new Link({href: '/', className: "flex items-center gap-2 hover:cursor-pointer user-select-none hover:transform hover:scale-95 transition-transform duration-200 ease-in-out", children: [_('img', {src: '/assets/img/ophose.png', alt: 'Ophose Logo', class: 'h-8 w-8'}),_('h1', {className: "font-semibold text-lg drop-shadow-lg"}, 'Ophose')]}),// Icons _('div', {className: "flex items-center gap-3 text-xl ml-auto"},new Link({href: '/', className: "py-2 rounded-full hover:bg-gray-100", c: _('i', {className: "bi bi-house"})}),new Link({href: '/tutorial/getting-started', className: "py-2 rounded-full hover:bg-gray-100", c: _('i', {className: "bi bi-book"})}),new Link({href: '/docs', className: "py-2 rounded-full hover:bg-gray-100", c: _('i', {className: "bi bi-file-earmark-text"})}),new Link({href: '/community', className: "py-2 rounded-full hover:bg-gray-100", c: _('i', {className: "bi bi-people"})}),new Link({href: '/r', className: "py-2 rounded-full hover:bg-gray-100", c: _('i', {className: "bi bi-collection"})}),new HeaderUserButton()),);} return _('header', {className: "py-3 sticky top-0 bg-white shadow-md z-30 transition duration-200 ease-in-out w-full"},// Responsive menu _responsiveMenu(),_('div', {className: "flex items-center container mx-auto gap-8 hidden lg:flex"},// Logo and title new Link({href: '/', className: "flex items-center gap-2 hover:cursor-pointer user-select-none hover:transform hover:scale-95 transition-transform duration-200 ease-in-out", children: [_('img', {src: '/assets/img/ophose.png', alt: 'Ophose Logo', class: 'h-8 w-8'}),_('h1', {className: "font-semibold text-lg drop-shadow-lg"}, 'Ophose')]}),// Navigation _('nav', {className: "flex items-center gap-2 font-semibold ml-auto"},new Link({href: "/tutorial/getting-started", className: "px-3 py-2 rounded-full hover:bg-gray-100", children: "Tutorials"}),_('a', {href: "/docs/", className: "px-3 py-2 rounded-full hover:bg-gray-100", children: "Documentation"}),new Link({href: "/community", className: "px-3 py-2 rounded-full hover:bg-gray-100", children: "Community"}),new Link({href: "/r", className: "px-3 py-2 rounded-full hover:bg-gray-100", children: "Resources"})),// Dark mode, Language, Github and User _('div', {className: "flex items-center gap-1 text-xl"},_('a', {href: 'https://github.com/ah-4/ophose-release', className: "py-2 px-3 rounded-full hover:bg-gray-100"}, _('i', {className: "bi bi-github"})),new HeaderUserButton()) ));} }class Footer extends Ophose.Component {constructor(props) {super(props);} render() {return _('footer', {className: "bg-gray-100 py-16"},_('nav', {className: "container mx-auto grid grid-cols-2 lg:grid-cols-5 gap-4"},// Ophose _('div', {className: "flex flex-col gap-2"},_('h2', {className: "text-lg font-semibold flex gap-2 items-center"},_('img', {src: "/assets/img/ophose.png", className: "h-6 w-6 object-contain"}),_('span', "AH4 introduces ",_('span', {className: "text-indigo-500"}, "Ophose")) ),_('p', {href: "#", className: "text-xs font-bold text-gray-600"}, "©2024"),_('p', {href: "#", className: "text-xs font-normal text-gray-600"}, "do less; create more."),),// Tutorials _('div', {className: "flex flex-col gap-3 text-sm text-gray-500 font-semibold"},_('h2', {className: "text-lg font-bold text-gray-700 mb-2"}, "Tutorials"),_('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")),// Documentation _('div', {className: "flex flex-col gap-3 text-sm text-gray-500 font-semibold"},_('h2', {className: "text-lg font-bold text-gray-700 mb-2"}, "Documentation"),_('a', {href: "#"}, "Components"),_('a', {href: "#"}, "Pages"),_('a', {href: "#"}, "Environments"),_('a', {href: "#"}, "Database"),_('a', {href: "#"}, "Authentication"),_('a', {href: "#"}, "Models"),_('a', {href: "#"}, "Security"),_('a', {href: "#"}, "API"),_('a', {href: "#"}, "Configuration"),),// Resources _('div', {className: "flex flex-col gap-3 text-sm text-gray-500 font-semibold"},_('h2', {className: "text-lg font-bold text-gray-700 mb-2"}, "Resources"),_('a', {href: "#"}, "Browse components"),_('a', {href: "#"}, "Browse environments")),// More _('div', {className: "flex flex-col gap-3 text-sm text-gray-500 font-semibold"},_('h2', {className: "text-lg font-bold text-gray-700 mb-2"}, "More"),_('a', {href: "#"}, "About"),_('a', {href: "#"}, "Contact"),_('a', {href: "#"}, "Pricing"),_('a', {href: "#"}, "Terms of service"),_('a', {href: "#"}, "Privacy policy"),_('div', {className: "flex gap-2 items-center text-2xl"},_('i', {className: "bi bi-github text-gray-500"}),_('i', {className: "bi bi-youtube text-gray-500"}),) ),) );} }class UTabs extends Ophose.Component {constructor(props) {super(props);this.selectedTab = live(this.props.defaultTab || this.props.tabs[0].name);} style() {return /* css */` %self { } ` } render() {return _('div',dyn(this.selectedTab, (selectedTab) => {return _('ul', {className: "flex flex-wrap"},this.props.tabs.map((tab, index) => {return _('a', {className: "px-4 py-2 cursor-pointer font-semibold " + (selectedTab == tab.name ? "border-b-2 border-indigo-500" : ""),onclick: () => this.selectedTab.set(tab.name)},tab.value );})) }),dyn(this.selectedTab, (selectedTab) => {return this.props.children[selectedTab]();}));} }class UPaginate extends Ophose.Component {constructor(props) {super(props);if(!this.props.mockData && !this.props.endpoint) dev.error("UPaginate requires either mockData or endpoint prop");this.data = live(null);this.query = live('');this.totalPages = live(1).min(1);this.page = live(1).min(1).max(this.totalPages);if(this.props.mockData) {this.data = live({totalPages: 1,count: this.props.mockData.length,rows: this.props.mockData });} else if (this.props.endpoint) {watch(this.query, this.page, (query, page) => {oenv(this.props.endpoint, {query, page}).then(data => {this.data.set(data);this.totalPages.set(data.totalPages);});});} this.columns = live(this.props.columns || []);} render() {return [// Search and pagination _('div', {className: "flex gap-4 justify-between items-center"},_('input', {placeholder: "Search...", className: "py-2 px-4 rounded-full bg-gray-50 border border-gray-200", oninput: (e) => {this.query.set(e.target.value);}}),_('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)}, ">"),),),dyn(this.data, d => {if(d === null) return _('div', {className: "text-center text-gray-500"}, 'Loading...');if(d.rows.length === 0) return _('div', {className: "text-center text-gray-500"}, 'No articles found.');return _('div', {className: this.props.className || "grid grid-cols-2 gap-4"},d.rows.map(article => this.props.parser(article))) })] }}class Auth {/** * Returns if the user is logged in or not * @returns {Promise} returns true if the user is logged in, false otherwise */ static async isLogged() {let logged = await oenv('ah4/auth/user');return (logged && logged.user) ? true : false;} /** * Returns the user data * @returns {Promise} returns the user data */ static async user() {let response = await oenv('ah4/auth/user');return response && response.user;} /** * Logs out the user */ static async logout() {return await oenv('ah4/auth/logout');} /** * Redirects to the login page if the user is not logged in * @param {string} url the url to redirect to if the user is not logged in */ static redirectIfNotLogged(url = '/login') {Auth.isLogged().then((logged) => {if (!logged) {route.go(url)} });} }const lives = {user: live(null)};oimpe('AH4/Auth');oimpc('base/Header');oimpc('base/Footer');oimpc('@/AH4/Starter/Link');oimpc('@/AH4/Starter/Blocker');oimpc('@/NUI/Fast/UTabs');oimpc('@/NUI/Fast/UTable');oimpc('@/NUI/Fast/UPaginate');oimpc('@/AH4/Starter/XForm');oimpc('@/AH4/Starter/XInput');class Base extends Ophose.Base {constructor(props) {super(props);// Load the user data Auth.user().then((user) => {lives.user.set(user);});importCss('https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css');importCss('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css');importScript("/assets/js/prism.js");importCss("/assets/css/prism.css");this.register('go', Link);this.register('u-tabs', UTabs);this.register('u-table', UTable);this.register('u-paginate', UPaginate);this.register('x-form', XForm);this.register('x-input', XInput);this.register('blocker', Blocker);} style() {return /* css */` * { scroll-margin-top: 6rem; scroll-behavior: smooth; } .container { padding-left: 1rem; padding-right: 1rem; } .loading { pointer-events: none; opacity: 0.5; } ` } render() {return _('main', {className: "text-gray-700"},new Header(),_('div', {className: "min-h-screen"},this.props.children ),new Footer()) }}