class Link extends Ophose.Component {constructor(props) {super(props);} style() {return ` %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 BackButtonTitle extends Ophose.Component {constructor(props) {super(props);} style() {return ` %self { } ` } render() {return {_: 'div', className: 'flex gap-2 xl:gap-4 text-xl xl:text-3xl items-center select-none', children: [new Link({href: this.props.url ?? '/', children: '<', className: 'text-neutral-500'}),_('h1', {className: 'font-bold'}, this.props.title)]}} }class PaymentButton extends Ophose.Component {constructor(props) {super(props, {paymentEndpoint: null,paymentData: null });this.paymentEndpoint = props.paymentEndpoint;} style() {return ` %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 BeatCreateChoose extends Ophose.Component {constructor(props) {super(props);this.choices = this.props.choices;this.selected = new Live(null);} style() {return ` %self { } ` } render() {let cols = this.props.cols ?? 8;let height = this.props.height ?? 24;let fontSize = this.props.fontSize ?? "text-md";return new PlacedLive(this.selected, (selected) => {return {_: 'div', className: 'w-full flex flex-col gap-2', children: [_('label', {className: 'text-xs text-neutral-500 uppercase'}, this.props.title ?? 'Choose a title'),_('div', {className: `grid grid-cols-1 xl:grid-cols-${cols} gap-4 select-none`}, Object.keys(this.choices).map((key) => {let choice = this.choices[key];let cost = choice.cost ?? false;let isSelected = selected && selected.key === key;let selectedClass = isSelected ? 'border-2 border-indigo-500 opacity-100' : 'opacity-50 hover:opacity-100';return {_: 'div', className: `cursor-pointer flex flex-col items-center gap-2 w-full h-${height} relative rounded-xl overfolw-hidden ` + selectedClass, children: [_('div', {className: 'font-bold w-full h-full relative flex flex-col items-center justify-center p-4 gap-2 ' + fontSize},_('p', choice.name, cost !== false && _('span', {className: 'text-indigo-500'}, ` +${choice.cost}`, _('sup', {className: 'bi bi-coin ml-1'}))),choice.description && _('p', {className: 'text-xs'}, choice.description)),_('img', {src: choice.image, className: 'w-full h-full absolute object-cover rounded-xl opacity-25 z-0'}),], onclick: () => {this.selected.set({key: key,choice: choice });}}}))]}})} } class BeatPreview extends Ophose.Component {constructor(props) {super(props);} render() {return {_: 'div', className: 'w-full p-4 rounded-2xl bg-neutral-900', children: [{_: 'div', className: 'flex justify-between items-center gap-4', children: [{_: 'div', className: 'flex gap-4 items-center', children: [{_: 'img', src: this.props.author.image, className: 'w-16 h-16 bg-neutral-950 rounded-2xl'},{_: 'div', className: 'flex flex-col gap-2', children: [{_: 'p', className: 'font-bold text-2xl', children: this.props.title},{_: 'p', className: 'text-neutral-400', children: `Preview / ${this.props.metadata.bpm} BPM / ${this.props.metadata.key} ${this.props.metadata.scale}`}]}]},{_: 'div', className: 'flex gap-4 items-center', children: [{_: 'i', className: 'bi bi-play-circle-fill text-4xl text-indigo-500 cursor-pointer hover:text-indigo-600', onclick: () => {currentMusicPlayer.playTrack(this.props);}}]}]}]}} };class BeatCreate extends Ophose.Component {constructor(props) {super(props);this.inspiration = new BeatCreateChoose({title: 'Choose your inspiration',choices: {"lucki": {name: "Lucki", image: "/img/singer.jpg"}} });this.beatStructure = new BeatCreateChoose({title: 'Choose your track structure',cols: 3,height: 48,fontSize: "text-xl",choices: {"classic": {name: "Classic",image: "/img/singer.jpg",description: "Fading slowly in and out, with a classic melody but efficient pattern. Recommended for most cases.",cost: 0 },"banger": {name: "Banger",image: "/img/singer.jpg",description: "Make your listeners go wild with a banger pattern with some effects and transitions. Recommended for club and hype beats.",cost: 2 },"full": {name: "Full",image: "/img/singer.jpg",description: "A full structure, with all the instruments and effects. Recommended for a full sound.",cost: 5 }} });this.beatFormat = new BeatCreateChoose({title: 'Choose your beat format',cols: 2,height: 24,fontSize: "text-xl",choices: {"mp3": {name: ".mp3",image: "/img/singer.jpg",description: "A classic format, compatible with most devices and platforms. Recommended for most cases.",cost: 2 },"wav": {name: ".wav",image: "/img/singer.jpg",description: "A high quality format, with lossless audio. Recommended for professional use.",cost: 6 },/* "stems": {name: ".zip (stems)",image: "/img/singer.jpg",description: "A .zip file with all the stems of your beat. Recommended for commercial licenses and professional use.",cost: 10 } */ }});this.beatPrivacy = new BeatCreateChoose({title: 'Choose your beat privacy',cols: 2,height: 24,fontSize: "text-xl",choices: {"public": {name: "Public",image: "/img/singer.jpg",description: "Your beat will be public and available for everyone to listen.",cost: 0 },"private": {name: "Private",image: "/img/singer.jpg",description: "Your beat will be private and only available for you to listen and download.",cost: 2 }} });this.name = new Live('');this.bpm = new Live('140');this.key = new Live('C');this.scale = new Live('major');this.previewMessage = new Live('');this.currentPreview = new Live(null);this.update = (e, live) => {live.set(e.target.value);} let toggleButtons = (buttonId) => {let button = document.getElementById(buttonId);if(!button) return;button.disabled = !button.disabled;button.style.opacity = button.disabled ? 0.5 : 1;} this.onPreview = (e) => {let target = e.target;while(target.tagName != 'BUTTON') target = target.parentElement;if(!lives.user.value) {this.previewMessage.set('You need to be logged in to generate a preview.');return;} this.previewMessage.set('Generating preview...');toggleButtons('preview_button');let data = {name: this.name,privacy: this.beatPrivacy.selected.value?.key,metadata: {key: this.key,scale: this.scale,inspiration: this.inspiration.selected.value?.key,structure: this.beatStructure.selected.value?.key,bpm: parseInt(this.bpm.value),format: this.beatFormat.selected.value?.key,} } oenv('yzuvai/track/create_preview', data).then((res) => {updateInfos();this.previewMessage.set('Preview generated! You can listen to it above.');this.currentPreview.set(res);}).catch((err) => {this.previewMessage.set(err.responseText)}).finally(() => {toggleButtons('preview_button');});};this.onGenerate = (e) => {let target = e.target;while(target.tagName != 'BUTTON') target = target.parentElement;if(!lives.user.value) {this.previewMessage.set('You need to be logged in to generate a beat.');return;} this.previewMessage.set('Generating beat...');toggleButtons('preview_button');toggleButtons('generate_button');oenv('yzuvai/track/create/' + this.currentPreview.value.id).then((res) => {updateInfos();this.previewMessage.set(_('span', 'Beat generated! It is now available for download ', new Link({href: '/track/' + this.currentPreview.value.id, className: 'text-indigo-500', c: 'on this page'})));this.currentPreview.set(res);}).catch((err) => {this.previewMessage.set(err.responseText)}).finally(() => {toggleButtons('preview_button');toggleButtons('generate_button');});} this.randomize = () => {let chooseRandom = (beatCreateChoose) => {let keys = Object.keys(beatCreateChoose.choices);let randomKey = keys[Math.floor(Math.random() * keys.length)];beatCreateChoose.selected.set({key: randomKey,choice: beatCreateChoose.choices[randomKey]});} chooseRandom(this.inspiration);chooseRandom(this.beatStructure);chooseRandom(this.beatFormat);chooseRandom(this.beatPrivacy);document.getElementById('input_bpm').value = Math.floor(Math.random() * 110) + 90;document.getElementById('input_bpm').dispatchEvent(new Event('input'));document.getElementById('input_key').value = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'][Math.floor(Math.random() * 12)];document.getElementById('input_key').dispatchEvent(new Event('change'));document.getElementById('input_scale').value = ['major', 'minor', 'aeolian'][Math.floor(Math.random() * 3)];document.getElementById('input_scale').dispatchEvent(new Event('change'));} } style() {return ` %self { } ` } render() {let summaryItem = (title, liveVar) => {return new PlacedLive(liveVar, (value) => {let displayValue = 'None';if(value && value.choice) displayValue = value.choice.name;else if(value) displayValue = value;return _('div', {className: 'flex gap-2 justify-between w-full'},_('div', {className: 'flex flex-col gap-2'},_('p', {className: 'text-xs text-neutral-400'}, title),_('p', {className: 'font-bold'}, displayValue)),_('div', {className: 'flex flex-col gap-2 text-indigo-500 font-bold'},value && value.choice && value.choice.cost ? _('a', value.choice.cost, _('span', {className: 'bi bi-coin ml-1'})) : 'Free' ));})};return {_: 'div', className: 'flex flex-col gap-8', children: [_('div', {className: 'flex justify-between'},_('button', {className: 'px-4 py-2 hover:bg-neutral-800 bg-neutral-900 text-white rounded-xl font-bold', onclick: () => this.randomize()}, 'Randomize'),_('button', {className: 'px-4 py-2 hover:bg-indigo-400 bg-indigo-500 text-white rounded-xl font-bold', onclick: () => {document.getElementById('beat-preview').scrollIntoView({behavior: 'smooth'});}}, 'Preview')),this.inspiration,_('div', {className: 'grid grid-cols-1 xl:grid-cols-8 gap-8'},_('div', {className: 'flex flex-col gap-2 col-span-5 xl:col-span-5'},_('label', {className: 'text-xs text-neutral-500 uppercase'}, 'Beat name'),_('input', {className: 'p-4 bg-neutral-900 text-white rounded-xl', placeholder: 'Enter a name for your beat...', oninput: (e) => this.update(e, this.name)})),_('div', {className: 'flex flex-col gap-2 col-span-5 xl:col-span-3'},_('label', {className: 'text-xs text-neutral-500 uppercase'}, 'Beat seed'),_('input', {className: 'p-4 bg-neutral-900 text-white rounded-xl opacity-50', placeholder: 'Seed (automatically generated)', disabled: true}),),_('div', {className: 'flex flex-col gap-2 col-span-5 xl:col-span-4'},_('label', {className: 'text-xs text-neutral-500 uppercase'}, 'BPM'),_('input', {id: 'input_bpm', className: 'p-4 bg-neutral-900 text-white rounded-xl', placeholder: 'Enter the BPM of your beat...', type: 'number', value: 140, oninput: (e) => this.update(e, this.bpm)})),_('div', {className: 'flex flex-col gap-2 col-span-5 xl:col-span-2'},_('label', {className: 'text-xs text-neutral-500 uppercase'}, 'Key'),_('select', {id: 'input_key', className: 'p-4 bg-neutral-900 text-white rounded-xl', placeholder: 'Enter the key of your beat...', onchange: (e) => this.update(e, this.key)},_('option', {value: 'C'}, 'C'),_('option', {value: 'C#'}, 'C#'),_('option', {value: 'D'}, 'D'),_('option', {value: 'D#'}, 'D#'),_('option', {value: 'E'}, 'E'),_('option', {value: 'F'}, 'F'),_('option', {value: 'F#'}, 'F#'),_('option', {value: 'G'}, 'G'),_('option', {value: 'G#'}, 'G#'),_('option', {value: 'A'}, 'A'),_('option', {value: 'A#'}, 'A#'),_('option', {value: 'B'}, 'B'),) ),_('div', {className: 'flex flex-col gap-2 col-span-5 xl:col-span-2'},_('label', {className: 'text-xs text-neutral-500 uppercase'}, 'Scale'),_('select', {id: 'input_scale', className: 'p-4 bg-neutral-900 text-white rounded-xl', placeholder: 'Enter the scale of your beat...', onchange: (e) => this.update(e, this.scale)},_('option', {value: 'major'}, 'Major'),_('option', {value: 'minor'}, 'Minor'),_('option', {value: 'aeolian'}, 'Aeolian')) ),),this.beatStructure,this.beatFormat,this.beatPrivacy,_('div', {className: 'grid grid-cols-2 xl:grid-cols-4 gap-8 mt-8', id: 'beat-preview'},_('div', {className: 'flex flex-col gap-4 col-span-2'},_('h1', {className: 'text-2xl font-bold'}, 'Preview'),_('p', {className: 'text-xs text-neutral-400'}, 'Click on the preview button to generate a clip of your beat. Note that all beat clips generated are tagged and are not usable for commercial purposes. If you want to use your beat for commercial purposes, you will need to generate it paying a fee with tokens. By generating a beat, you agree to our terms of use and privacy policy. Please note that the preview will be available for 24 hours and will be deleted after that. Also if you re-generate the beat with same title, the preview will be replaced by the new one.'),new PlacedLive(this.currentPreview, (currentPreview) => {if(currentPreview) return new BeatPreview(currentPreview);}),_('p', {className: 'text-xs text-neutral-400'}, 'Once, you purchased a beat, you will be able to download the full version of the beat in the format you chose. Note that the beat will remain available for download for 1 month. After that, you will need to re-create the beat to download it again. As the AI model is constantly evolving, the beat you re-create may be different from the original one.'),_('button', {id: 'preview_button', className: 'px-4 py-2 hover:bg-indigo-400 bg-indigo-500 text-white rounded-xl font-bold', onclick: (e) => this.onPreview(e)},'Preview', _('span', ' (1 token)')),_('p', {className: 'text-xl text-neutral-500'}, new PlacedLive(this.previewMessage, (message) => message)),_('p', {className: 'text-xl font-bold text-neutral-300'}, 'Free previews left today: ', _('span', {className: 'text-white'}, lives.freeTokenRemaining)),_('div', {className: 'bg-gradient-to-r from-pink-800 to-indigo-800 p-4 rounded-xl flex flex-col gap-2 font-bold'},_('p', {className: 'text-xl'}, 'Up to 10 free previews per day for Premium members!'),_('p', {className: 'text-xs'}, 'Click here to get a Premium membership for only 14.99$ per month.'),),_('p', {className: 'text-xs text-neutral-400'}, 'In order to generate a beat, you need to have enough tokens in your account. If you don\'t have enough tokens, you can buy some by clicking on the token icon in the header. You also need to preview the beat before generating it to make sure it fits your needs.'),new PlacedLive(this.currentPreview, (currentPreview) => {if(!currentPreview) return _('button', {className: 'px-4 py-2 bg-neutral-500 text-white rounded-xl font-bold', disabled: true}, 'Generate') return _('button', {id: 'generate_button', className: 'px-4 py-2 bg-indigo-500 text-white rounded-xl font-bold hover:bg-indigo-400', onclick: (e) => this.onGenerate(e)}, 'Generate (', currentPreview.price, _('span', {className: 'bi bi-coin ml-1'}), ')')})),_('div', {className: 'flex flex-col gap-4 col-span-2'},_('div', {className: 'flex flex-col gap-4 p-8 bg-neutral-900 rounded-xl select-none mt-8'}, _('h2', {className: 'text-xs text-neutral-500 uppercase'}, 'Beat summary'), _('hr', {className: 'border-neutral-700 w-full'}), summaryItem('Inspiration', this.inspiration.selected), _('hr', {className: 'border-neutral-700 w-full'}), summaryItem('Name', this.name), _('hr', {className: 'border-neutral-700 w-full'}),summaryItem('BPM', this.bpm),_('hr', {className: 'border-neutral-700 w-full'}),summaryItem('Key', this.key),_('hr', {className: 'border-neutral-700 w-full'}),summaryItem('Scale', this.scale),_('hr', {className: 'border-neutral-700 w-full'}), summaryItem('Structure', this.beatStructure.selected), _('hr', {className: 'border-neutral-700 w-full'}), summaryItem('Format', this.beatFormat.selected), _('hr', {className: 'border-neutral-700 w-full'}), summaryItem('Privacy', this.beatPrivacy.selected), _('hr', {className: 'border-neutral-700 w-full'}),new PlacedLive(this.beatFormat.selected, this.beatStructure.selected, this.inspiration.selected, this.beatPrivacy.selected, (format, structure, inspiration, privacy) => {let cost = 0;let costItem = (value) => {if(value && value.choice) cost += value.choice.cost ?? 0;} costItem(format);costItem(structure);costItem(inspiration);costItem(privacy);return _('div', {className: 'flex gap-2 justify-between w-full text-xl font-bold'}, _('h2', 'Generation cost'), _('h2', {className: 'text-indigo-500'}, cost, _('span', {className: 'bi bi-coin ml-2'}))) })) )) ]}} };class Beat extends Ophose.Component {constructor(props) {super(props);this.data = new Live(this.props);} style() {return ` %self .image-container .play-button { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 3rem; background: rgba(0, 0, 0, 0.5); border-radius: 50%; padding: 1rem; transition: all 0.3s; z-index: 1; } %self .image-container:hover .play-button { display: block; } %self .image-container:hover img { filter: brightness(0.5); } ` } render() {return _('div', {className: 'bg-neutral-900 p-4 rounded-xl flex flex-col gap-4'},_('div', {className: 'w-full rounded-xl cursor-pointer image-container relative', onclick: () => {currentMusicPlayer.playTrack(this.props);}},_('img', {src: this.props.author.image, className: 'w-full rounded-xl object-cover aspect-square cursor-pointer'}),_('i', {className: 'bi bi-play-circle-fill text-white play-button'}),_('div', {className: 'absolute top-4 right-4 flex items-center gap-2'},!this.props.published ? _('span', {className: 'bg-neutral-700 p-2 rounded-full text-xs top-4 right-4'}, 'Preview') : null,!this.props.published || this.props.private ? _('span', {className: 'bg-indigo-500 px-4 py-2 rounded-full text-md top-4 right-4 bi bi-lock-fill'}) : null )),_('div', {className: 'flex justify-between items-center'},new Link({href: '/track/' + this.props.id,children: _('div', {className: 'flex gap-4 items-center py-2 px-4 bg-neutral-900 rounded-full cursor-pointer hover:bg-neutral-800'},_('img', {src: this.props.author.image, className: 'w-12 h-12 rounded-full object-cover'}),_('div', {className: 'flex flex-col items-start'},_('h2', this.props.title),_('p', {className: 'text-xs text-neutral-500'}, this.props.author.username)) )}),new PlacedLive(this.data, (data) => {return _('div', {className: 'flex gap-2 items-center p-2 hover:bg-neutral-800 rounded-full cursor-pointer', onclick: () => {oenv('yzuvai/track/like/' + data.id).then(data => {let like = this.data.value.like;if(data.liked) {like.count++;} else {like.count--;} like.liked = data.liked;this.data.set({...this.data.value,like });});}},_('i', {className: 'bi text-indigo-500 ' + (data.like.liked ? 'bi-heart-fill' : 'bi-heart')}),_('p', data.like.count)) })),_('p', {className: 'text-xs text-neutral-500 text-left pl-4'}, '@' + this.props.seed),_('p', {className: 'text-xs text-neutral-400 text-left pl-4'}, `${this.props.metadata.bpm} BPM / ${this.props.metadata.key} ${this.props.metadata.scale}`),_('div', {className: 'flex flex-wrap gap-2 px-4'},_('span', {className: 'bg-indigo-500 p-2 rounded-full text-xs'}, '#', this.props.metadata.inspiration)) );} };class BeatSection extends Ophose.Component {constructor(props) {super(props);this.props = {title: "Beat section title",endpoint: '/api/yzuvai/tracks/all',...this.props } this.beats = new Live(null);this.currentPage = new Live(1);this.totalPages = new Live(1);this.sortBy = new Live('newest');this.getData = () => {oenv(this.props.endpoint, {page: this.currentPage,perPage: 12,sortBy: this.sortBy }).then(data => {this.beats.set(data);this.totalPages.set(data.totalPages);}).catch((err) => {this.beats.set(null);});} this.getData();this.changeSortThenReload = (newSort) => {this.sortBy.set(newSort);this.currentPage.set(1);this.getData();};} style() {return ` %self { } ` } render() {return _('div', {className: 'flex flex-col gap-8'},_('div', {className: 'flex items-center justify-between'},_('h2', {className: 'font-bold text-xl xl:text-6xl'}, this.props.title),_('div', {className: 'flex gap-2 xl:gap-4 items-center font-bold text-neutral-400'},_('button', {className: 'px-4 py-2 bg-neutral-700 text-white rounded-xl', onclick: () => {if(this.currentPage > 1) {this.currentPage.remove(1);this.getData();} }}, '<'),_('span', this.currentPage),'/',_('span', this.totalPages),_('button', {className: 'px-4 py-2 bg-neutral-700 text-white rounded-xl', onclick: () => {if(this.currentPage < this.totalPages) {this.currentPage.add(1);this.getData();} }}, '>')) ),_('div', {className: 'flex gap-2 xl:gap-4 items-center font-bold text-neutral-400'},new PlacedLive(this.sortBy, (sortBy) => {let createOption = (option) => {return _('a', {className: `px-4 py-2 bg-${sortBy == option ? 'indigo-500' : 'neutral-900'} text-white rounded-3xl cursor-pointer`, onclick: () => this.changeSortThenReload(option)}, option.charAt(0).toUpperCase() + option.slice(1));} return _('div', {className: 'flex gap-4 items-center gap-4'},createOption('newest'),createOption('oldest')) }),_('a', {className: 'px-4 py-2 text-neutral-400'}, '(', new PlacedLive(this.beats, (beats) => beats?.count),' results)'),),new PlacedLive(this.beats, (beats) => {if(!beats) return _('div', {className: 'text-3xl text-center text-neutral-400'}, 'Loading...');if(beats.count === 0) return _('div', {className: 'text-3xl text-center text-neutral-400'}, 'No beats found');return _('div', {className: 'grid grid-cols-1 xl:grid-cols-4 gap-8'},beats.rows.map(beat => new Beat(beat))) }));} }class Blocker extends Ophose.Component {constructor(props) {super(props);} style() {return ` %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 )]}} }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 ` %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) return;let submitButton = form.querySelector('button[type="submit"], input[type="submit"]');submitButton.classList.add('loading');oenv(url, formData) .then(r => {if(this.props.onSuccess) this.props.onSuccess(r);this.updateErrors([]);submitButton.classList.remove('loading');}) .catch(e => {submitButton.classList.remove('loading');let errors = e.responseJSON && e.responseJSON.errors || null;if(errors) {this.updateErrors(errors);} if(this.props.onError) this.props.onError(e);});} render() {return _('form', {onsubmit: this.onSubmit.bind(this)}, this.props.children);} }class XInput extends Ophose.Component {constructor(props) {super(props);this.propsOn('input');this.typesAsTag = ['select', 'textarea'];if(!this.props.type) this.props.type = 'text';if(!this.props.id) this.props.id = this.props.name;if(!this.props.label) this.props.label = this.props.name.charAt(0).toUpperCase() + this.props.name.slice(1).replace('_', ' ');if(!this.props.placeholder) this.props.placeholder = this.props.label;this.tag = this.typesAsTag.includes(this.props.type) ? this.props.type : 'input';this.value = new Live(this.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 ` %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 Register extends Ophose.Component {constructor(props) {super(props);} render() {return new XForm({className: 'rounded-3xl shadow-md flex flex-col gap-2 xl:gap-8 text-white',endpoint: '/@/ah4/auth/register',onSuccess: () => {this.appendChild(_('div', {className: 'bg-green-500 p-4 rounded-3xl shadow-md'}, 'Account has been registered ! Check your e-mails to confirm your mail address.'));},children: [_('h1', {className: 'text-xl xl:text-5xl font-bold'}, 'Create a new account'),new XInput({name: 'username', maxlength: 32, placeholder: 'Username', className: 'w-full p-4 my-1 rounded-3xl shadow-md bg-neutral-800'}),new XInput({name: 'email', type: 'email', placeholder: 'Email', className: 'w-full p-4 my-1 rounded-3xl shadow-md bg-neutral-800'}),new XInput({name: 'password', type: 'password', placeholder: 'Password', className: 'w-full p-4 my-1 rounded-3xl shadow-md bg-neutral-800'}),new XInput({name: 'password_confirm', type: 'password', placeholder: 'Confirm password', className: 'w-full p-4 my-1 rounded-3xl shadow-md bg-neutral-800'}),_('p', {className: 'text-xs text-neutral-500'}, 'By registering, you agree to our ', new Link({href: '/terms', className: 'text-indigo-500', c: 'terms and conditions'}), '.'),_('button', {type: 'submit', className: 'bg-indigo-500 p-4 rounded-3xl shadow-md cursor-pointer'}, 'Register')] })} };class Login extends Ophose.Component {constructor(props) {super(props);} render() {return new XForm({className: 'rounded-3xl shadow-md flex flex-col gap-2 xl:gap-8 text-white',endpoint: '/@/ah4/auth/login',onSuccess: () => {updateInfos();this.findFirstParentComponentOfType(AuthPanel).remove();},children: [_('h1', {className: 'text-xl xl:text-5xl font-bold'}, 'Login to your account'),new XInput({name: 'username', placeholder: 'Username or e-mail', label: 'Username or e-mail', className: 'w-full p-4 my-1 rounded-3xl shadow-md bg-neutral-800'}),new XInput({name: 'password', type: 'password', placeholder: 'Password', className: 'w-full p-4 my-1 rounded-3xl shadow-md bg-neutral-800'}),_('button', {type: 'submit', className: 'bg-indigo-500 p-4 rounded-3xl shadow-md cursor-pointer'}, 'Login')] })} };class AuthPanel extends Ophose.Component {constructor(props) {super(props);this.form = new Live("register");} style() {return ` %self .errors { color: red; } @media (max-width: 768px) { %self label { display: none; } } ` } render() {return new Blocker({children: [_('div', {className: 'bg-neutral-950 xl:rounded-3xl shadow-md flex flex-col gap-8 text-white justify-center items-center xl:px-16 xl:py-12 px-4 py-8 xl:w-auto w-screen z-30 scale-75'},new PlacedLive(this.form, (form) => {let selectedClass = 'bg-indigo-500';let onClick = (form) => {if(form == this.form.value) return;this.form.set(form);} return _('div', {className: 'flex gap-2 p-4 bg-neutral-900 rounded-3xl shadow-md text-white cursor-pointer select-none'}, [_('p', {className: 'px-4 py-2 rounded-xl ' + (form === "register" && selectedClass), onclick: () => onClick("register")}, 'Register'),_('p', {className: 'px-4 py-2 rounded-xl ' + (form === "login" && selectedClass), onclick: () => onClick("login")}, 'Login')])}),new PlacedLive(this.form, (form) => {if(form === "register") return new Register();if(form === "login") return new Login();})) ]})} };class UserSettings extends Ophose.Component {constructor(props) {super(props);this.billings = new Live([]);this.billingsPage = new Live(1);this.currentSubscription = new Live(null);this.loadBilling = () => {oenv('yzuvai/settings/billing', {page: this.billingsPage.value}).then((res) => {this.billings.set(res);});} this.loadBilling();oenv('yzuvai/subscription/current').then((subscription) => {this.currentSubscription.set(subscription);});} style() {return ` %self .errors { font-size: 0.75em; color: #ff6666; } ` } render() {let changesMessage = new Live('');let subscriptionMessage = new Live('');return _('div', {className: 'flex flex-col gap-16'},new PlacedLive(this.currentSubscription, (subscription) => {let plan = subscription && subscription.name || 'free';let daysLeft = subscription && (new Date(subscription.expires).getTime() - Date.now()) || null;let subscribeButton = (id) => {return new PaymentButton({paymentEndpoint: 'yzuvai/subscription/buy',className: 'px-4 py-2 bg-white text-black rounded-xl w-fit font-bold',paymentData: {subscription: id},children: 'Change to this plan',onPaymentFail: (res) => {res = res.responseJSON;subscriptionMessage.set(res.error);} })} return _('div', {className: 'flex flex-col gap-4'},_('h2', {className: 'text-sm font-bold text-neutral-600 uppercase'}, 'Plan'),_('p', {className: 'text-sm'}, 'You are currently on the ', _('b', plan), ' plan. ', subscription == null && 'Upgrade to get access to more features. ', subscription == null && new Link({href: '/subscription', className: 'text-indigo-500', children: 'Discover all plans.'}), subscription != null && daysLeft && _('span', {className: 'text-neutral-400'}, `Your subscription will expire in ${Math.floor(daysLeft / (1000 * 60 * 60 * 24))} days.`)),_('div', {className: 'grid grid-cols-1 xl:grid-cols-3 gap-4'},_('div', {className: 'flex flex-col gap-4 p-8 bg-neutral-900 rounded-3xl'},_('div', {className: 'flex justify-between items-center'},_('div', {className: 'flex flex-col gap-2'},_('h2', {className: 'text-2xl font-bold'}, 'Starter'),_('p', {className: 'text-sm text-neutral-600'}, 'Get access to restricted features of YzuvAI.'),),_('h2', {className: 'text-2xl font-bold text-white'}, 'Free'),),_('h3', {className: 'py-2 rounded-xl w-fit font-bold text-neutral-600'}, subscription == null ? 'Current plan' : 'You currently have another plan.'),),_('div', {className: 'flex flex-col gap-4 p-8 bg-gradient-to-r from-neutral-900 to-neutral-800 rounded-3xl'},_('div', {className: 'flex justify-between items-center'},_('div', {className: 'flex flex-col gap-2'},_('h2', {className: 'text-2xl font-bold'}, 'Basic'),_('p', {className: 'text-sm text-neutral-600'}, 'Get access to the basic features of the app.'),),_('h2', {className: 'text-2xl font-bold text-white'}, '$14.99', _('span', {className: 'text-sm text-neutral-400'}, '/month')),),subscription == null ? subscribeButton(1) : _('h3', {className: 'py-2 rounded-xl w-fit font-bold text-neutral-600'}, subscription.name == 'premium' ? 'You currently have another plan.' : 'Current plan')),_('div', {className: 'flex flex-col gap-4 p-8 rounded-3xl bg-gradient-to-r from-pink-500 to-indigo-600 text-white'},_('div', {className: 'flex justify-between items-center'},_('div', {className: 'flex flex-col gap-2'},_('h2', {className: 'text-2xl font-bold'}, 'Premium'),_('p', {className: 'text-sm text-white'}, 'Get access to the full features of the app.'),),_('h2', {className: 'text-2xl font-bold text-white'}, '$49.99', _('span', {className: 'text-sm text-neutral-400'}, '/month')),),subscription == null ? subscribeButton(2) : _('h3', {className: 'py-2 rounded-xl w-fit font-bold text-white'}, subscription.name == 'basic' ? 'You currently have another plan.' : 'Current plan')),_('h3', {className: 'py-4 xl:col-span-3 text-red-400'}, subscriptionMessage)) );}),_('div', {className: 'flex flex-col gap-4'},_('h2', {className: 'text-sm font-bold text-neutral-600 uppercase'}, 'Update Credentials'),new XForm({className: 'grid grid-cols-1 xl:grid-cols-2 gap-4', endpoint: '/@/yzuvai/settings/change', onSuccess: () => {changesMessage.set('Your changes have been saved.');updateInfos();}, c: [new XInput({name: 'username', label: 'Username', maxlength: 32, className: 'xl:col-span-2 rounded-3xl p-4 bg-neutral-800 text-white w-full my-1'}),new XInput({name: 'email', type: 'email', label: 'E-mail', className: 'xl:col-span-2 rounded-3xl p-4 bg-neutral-800 text-white w-full my-1'}),new XInput({name: 'current_password', type: 'password', label: 'Current password', className: 'xl:col-span-2 rounded-3xl p-4 bg-neutral-800 text-white w-full my-1'}),new XInput({name: 'password', type: 'password', label: 'Change password', className: 'xl:col-span-2 rounded-3xl p-4 bg-neutral-800 text-white w-full my-1'}),new XInput({name: 'password_confirm', type: 'password', label: 'Confirm password', className: 'xl:col-span-2 rounded-3xl p-4 bg-neutral-800 text-white w-full my-1'}),new XInput({name: 'pfp', label: 'Profile picture', type: 'file', className: 'block file:bg-neutral-800 file:border-0 file:text-neutral-500 xl:col-span-2 rounded-3xl p-4 bg-neutral-800 text-white w-full my-1'}),_('h3', {className: 'xl:col-span-2 text-green-400'}, changesMessage),_('div', {className: 'xl:col-span-2 flex justify-end'},_('input', {type: 'submit', className: 'px-4 py-2 bg-indigo-500 text-white rounded-xl w-fit font-bold cursor-pointer'}, 'Save changes')),]})),_('div', {className: 'flex flex-col gap-4'},_('div', {className: 'flex justify-between items-center py-2'},_('h2', {className: 'text-sm font-bold text-neutral-600 uppercase'}, 'Billing history'),_('div', {className: 'flex gap-2 items-center'},_('button', {className: 'px-4 py-2 bg-neutral-600 text-white rounded-xl w-fit font-bold', onclick: () => {let page = this.billingsPage.value;if(page == 1) return;this.billingsPage.set(page - 1);this.loadBilling();}}, 'Previous'),this.billingsPage,'/',new PlacedLive(this.billings, billing => billing.totalPages || 1),_('button', {className: 'px-4 py-2 bg-neutral-600 text-white rounded-xl w-fit font-bold', onclick: () => {let page = this.billingsPage.value;if(page >= this.billings.value.totalPages) return;this.billingsPage.set(page + 1);this.loadBilling();}}, 'Next'),) ),_('table', {className: 'w-full table-auto text-white'},_('thead', {className: 'text-left bg-neutral-900'},_('tr',_('th', {className: 'p-4'}, 'Date'),_('th', {className: 'p-4'}, 'Item'),_('th', {className: 'p-4'}, 'Amount'),_('th', {className: 'p-4'}, 'Status'),) ),new PlacedLive(this.billings, (billings) => {if(billings.count == 0) return _('tbody', {className: 'col-span-4 relative w-full'}, _('tr', {className: ''}, _('td', {className: 'w-full p-4 text-center', colSpan: 4}, 'No billing history.')));return _('thead', billings.rows && billings.rows.map(billing => _('tr',_('td', {className: 'p-4'}, new Date(billing.date).toLocaleDateString()),_('td', {className: 'p-4'}, billing.item),_('td', {className: 'p-4'}, _('b', '$', billing.price)),_('td', {className: 'p-4 text-neutral-500'}, 'Paid')))) })) )) }}class Sidebar extends Ophose.Component {constructor(props) {super(props);this.currentLink = new Live(window.location.pathname);Ophose.Event.addListener("onPageLoad", (url) => {this.currentLink.set(url);})} style() {return ` %self { } ` } render() {let createLink = (url, href, title, icon, iconActive, color = 'text-white') => {let isActive = url === href || (url !== '/' && url !== '' && href.startsWith(url));return new Link({href, className: `${color} p-2 text-sm xl:text-lg rounded-xl hover:bg-neutral-900 ${isActive ? 'font-bold bg-neutral-900' : ''}`, children: [_('i', {className: `bi bi-${isActive ? iconActive + ' text-indigo-500' : icon} mr-2`}),title ]})} return {_: 'aside', id: 'sidebar', className: 'max-h-screen overflow-y-auto h-screen border-r bg-neutral-950 border-neutral-700 select-none xl:flex hidden flex-col absolute xl:static z-20', children: [_('div', {className: 'p-2 xl:p-4 flex items-center justify-between'},_('h1', {className: 'text-2xl font-bold'}, 'YZUV', _('sup', {className: 'text-indigo-500'}, 'AI')),_('i', {className: 'bi bi-x text-2xl cursor-pointer xl:hidden', onclick: () => {let sidebar = document.getElementById('sidebar');sidebar.style.left = '-100%';sidebar.style.display = 'none';}})),_('div', {className: 'p-2 xl:p-4 hidden xl:block'},_('div', {className: 'bg-gradient-to-r from-indigo-500 to-blue-500 p-4 rounded-xl flex flex-col gap-2 font-bold'},_('p', {className: 'text-xs'}, 'Create your own beat and share it with the world.'),new Link({href: '/create', className: 'p-2 bg-black text-white rounded-xl text-center', children: 'Generate'})) ),new PlacedLive(this.currentLink, lives.user, (link, user) => {return _('div', {className: 'mt-4'},_('div', {className: 'p-2 xl:p-4 flex flex-col gap-2'},_('h2', {className: 'text-xs text-neutral-400'}, 'Start Here'),createLink(link, '/', 'Home', 'house', 'house-fill'),createLink(link, '/create', 'Create', 'plus-circle', 'plus-circle-fill', 'text-indigo-500'),createLink(link, '/explore', 'Explore', 'compass', 'compass-fill'),createLink(link, '/subscription', 'Subscription', 'credit-card', 'credit-card-fill'),createLink(link, '/buy-token', 'Buy Token', 'wallet', 'wallet-fill'),createLink(link, '/about', 'About', 'info-circle', 'info-circle-fill')),user && _('div', {className: 'p-2 xl:p-4 flex flex-col gap-2'},_('h2', {className: 'text-xs text-neutral-400'}, 'Your Beats'),createLink(link, '/me/beats', 'My Beats', 'music-note-list', 'music-note-list'),createLink(link, '/me/likes', 'Liked Beats', 'heart', 'heart-fill'),) );}),new PlacedLive(lives.user, (user) => {if(!user) return _('div', {className: 'p-2 xl:p-4 mt-auto'},_('div', {className: 'flex gap-2 hover:bg-neutral-900 cursor-pointer rounded-xl p-4', onclick: () => {this.appendChild( new AuthPanel());}},_('i', {className: 'bi bi-person-circle text-4xl'}),_('div', {className: 'flex flex-col'},_('p', {className: 'text-sm font-bold'}, 'Anonymous'),_('p', {className: 'text-xs text-neutral-400'}, 'Click to login or register.')) ));return _('div', {className: 'p-2 xl:p-4 mt-auto flex flex-col gap-2'},new Link({href: '/user/' + user.username, className: 'flex gap-2 items-center hover:bg-neutral-900 cursor-pointer rounded-xl p-4', children: [_('img', {className: 'w-8 h-8 rounded-full', src: '/@/yzuvai/user/profile_picture/' + user.username, alt: 'User avatar'}),_('div', {className: 'flex flex-col'},_('p', {className: 'text-sm font-bold'}, user.username),_('p', {className: 'text-xs text-neutral-400'}, user.email)) ]}),_('div', {className: 'flex gap-2 items-center hover:bg-neutral-900 cursor-pointer rounded-xl py-2 px-4 text-red-500', onclick: () => {Auth.logout().then(() => {updateInfos();});}},_('i', {className: 'bi bi-box-arrow-left text-2xl'}),_('p', {className: 'text-sm font-bold'}, 'Logout')),) }),_('div', {className: 'p-4'},_('p', {className: 'text-xs text-neutral-400'}, '© 2024 YZUVAI - All rights reserved.')) ]}} }class Header extends Ophose.Component {constructor(props) {super(props);} style() {return ` %self { } ` } render() {return {_: 'div', className: 'flex justify-between items-center sticky top-0 bg-neutral-950 py-4 z-10', children: [_('div', {className: 'flex gap-4 items-center'},_('i', {className: 'bi bi-list text-2xl xl:hidden cursor-pointer', onclick: () => {let sidebar = document.getElementById('sidebar');sidebar.style.left = sidebar.style.left === '0px' ? '-100%' : '0px';sidebar.style.display = sidebar.style.display === 'flex' ? 'none' : 'flex';}}),_('h1', {className: 'text-2xl font-bold hidden xl:inline-flex'},'Yzuv',_('sup', {className: 'text-indigo-500 top-3'}, 'AI'),_('span', {className: 'font-normal text-neutral-500 hidden xl:block ml-1'}, ' - Generate type beats with AI')) ),_('div', {className: 'flex gap-4'},new Link({href: '/buy-token', className: 'p-4 font-bold bg-neutral-900 text-white rounded-xl flex items-center gap-2', children: [_('p', lives.tokenAmount),_('i', {className: 'bi bi-coin text-indigo-500'})]}),new Link({href: '/create', className: 'p-4 font-bold bg-indigo-500 text-white rounded-xl flex items-center gap-2', children: [_('p', 'Create')]})) ]}} }class CurrentMusicPlayer extends Ophose.Component {constructor(props) {super(props);this.data = new Live({title: "(no title)",tempo: 0,duration: "0:00",key: "C",scale: "Major" });this.isPlaying = new Live(false);this.updateTimer = (audio, slider) => setInterval(() => {slider.value = (audio.currentTime / audio.duration) * 10000;if(audio.paused || audio.ended) {this.isPlaying.set(false);if(audio.ended) {audio.currentTime = 0;audio.pause();slider.value = 0;} } if(this.isPlaying.value === false) {clearInterval(this.interval);} }, 100);} togglePlay(value) {let audio = document.getElementById('current_track');if(audio.paused) audio.play() else audio.pause();this.isPlaying.set(!audio.paused);if(this.isPlaying.value) {let slider = document.getElementById('current_track_slider');this.interval = this.updateTimer(audio, slider);} } playTrack(track) {this.getNode().classList.remove('hidden');let audio = document.getElementById('current_track');if(!audio.paused) {clearInterval(this.interval);} this.data.set(track);this.togglePlay();} render() {return {_: 'div', className: 'w-full p-4 border-t border-neutral-700 bg-neutral-900 sticky bottom-0 left-0 flex flex-col gap-4 mt-4 z-10 hidden', children: [{_: 'div', className: 'flex justify-between items-center gap-4', children: [{_: 'div', className: 'flex gap-4 items-center', children: [new PlacedLive(this.data, data => data.author && ({_: 'img', src: data.author?.image, className: 'w-12 h-12 bg-neutral-950 rounded-2xl'})),new PlacedLive(this.data, (data) => {if(!data) return _('div', {className: 'flex flex-col gap-2'}, 'Loading...');return {_: 'div', className: 'flex flex-col gap-2', children: [{_: 'p', className: 'font-bold text-xl', children: data.title},{_: 'p', className: 'text-sm text-neutral-400', children: ['Preview / ', data.metadata?.bpm, ' BPM / ', data.metadata?.key, ' ', data.metadata?.scale]}]}})]},{_: 'div', className: 'flex gap-4 items-center', children: [new PlacedLive(this.isPlaying, (isPlaying) => {return {_: 'i', className: 'bi text-3xl text-indigo-500 cursor-pointer ' + (isPlaying ? 'bi-pause-circle-fill' : 'bi-play-circle-fill'), onclick: () => {this.togglePlay();}}})]}]},{_: 'div', className: 'flex gap-4 items-center', children: [new PlacedLive(this.data, (data) => {return _('audio', {src: data.source, id: 'current_track', className: 'w-full', oncanplay: (e) => {let slider = document.getElementById('current_track_slider');slider.oninput = () => {let audio = document.getElementById('current_track');audio.currentTime = (slider.value / 10000) * audio.duration;} }});}),_('input', {type: 'range', id: 'current_track_slider', className: 'slider w-full ring:indigo-500 appearance-none bg-neutral-800 rounded-xl h-1', min: 0, max: 10000, value: 0, step: 1, disabled: true}),]}]}} }class SplashScreen extends Ophose.Component {constructor(props) {super(props);setTimeout(() => {this.remove();}, 2000);} style() {return ` %self { position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: #000000; z-index: 1000; width: 100%; height: 100%; color: white; font-family: 'Poppins', sans-serif; user-select: none; gap: 2rem; } %self h1 { font-size: 4rem; font-weight: 700; } %self .text { color: rgb(99, 102, 241); } %self p { font-size: 0.75rem; color: #aaa; } ` } render() {return {_: 'div', children: [_('h1', {className: 'text-white text-4xl'}, 'Yzuv', _('sup', {className: 'text'}, 'AI')),_('p', {className: 'text-white text-2xl'}, 'Loading... Almost there!')]}} }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;} 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: new Live(null),tokenAmount: new Live(0),freeTokenRemaining: new Live(0)} const updateInfos = () => {Auth.user().then((user) => {lives.user.set(user);});oenv('yzuvai/get_token_amount') .then((amount) => {lives.tokenAmount.set(parseInt(amount));});oenv('yzuvai/user/get_free_token_amount') .then((amount) => {lives.freeTokenRemaining.set(parseInt(amount));});} const currentMusicPlayer = new CurrentMusicPlayer();class Base extends Ophose.Base {constructor(props) {super(props);app.setTitle("Home - YZUVAI");app.setIcon('/favicon.ico');updateInfos();importScript("https://cdn.tailwindcss.com");importCss("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css");} style() {return ` * { scroll-margin-top: 4rem; } ` } render() {return {_: 'div', className: 'bg-neutral-950 min-h-screen text-white flex', c: [new SplashScreen(),new Sidebar({className: 'w-72'}),_('main', {className: 'flex-1 flex flex-col gap-2 xl:gap-4 h-screen overflow-y-auto'},new Header({className: 'px-4'}),{_: 'main',id: 'page',className: 'px-4 py-4',children: this.props.children },currentMusicPlayer )] }} }