add websites tab

This commit is contained in:
jelveh
2025-08-03 18:24:01 -07:00
committed by Nariman Jelveh
parent e0d7a4aa17
commit 7c30472bdf
6 changed files with 375 additions and 26 deletions
+17 -21
View File
@@ -35,7 +35,7 @@ body{
flex: 1;
}
h1 .app-count, h1 .worker-count{
h1 .app-count, h1 .worker-count, h1 .website-count{
font-size: 20px;
color: #6d767d;
font-weight: 400;
@@ -70,10 +70,6 @@ h1 .app-count, h1 .worker-count{
border-style: solid;
border-width: 1px;
line-height: 35px;
background: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#e1e1e1));
background: linear-gradient(#f6f6f6, #e1e1e1);
-webkit-box-shadow: inset 0px 1px 0px rgb(255 255 255 / 30%), 0 1px 2px rgb(0 0 0 / 15%);
box-shadow: inset 0px 1px 0px rgb(255 255 255 / 30%), 0 1px 2px rgb(0 0 0 / 15%);
border-radius: 4px;
outline: none;
@@ -204,7 +200,7 @@ h1 .app-count, h1 .worker-count{
padding: 0 24px;
}
.refresh-app-list, .refresh-worker-list {
.refresh-app-list, .refresh-worker-list, .refresh-website-list {
width:40px;
padding: 10px;
float: right;
@@ -217,7 +213,7 @@ h1 .app-count, h1 .worker-count{
opacity: 0.9;
}
.refresh-app-list:hover, .refresh-worker-list:hover {
.refresh-app-list:hover, .refresh-worker-list:hover, .refresh-website-list:hover {
opacity: 1;
}
@@ -260,7 +256,6 @@ section {
background: white;
overflow: hidden;
position: relative;
}
.app-card:hover, .website-card:hover, .worker-card:hover {
@@ -457,7 +452,7 @@ label {
left: 0;
z-index: 1000;
display: block;
padding: 30px 16px;
padding: 30px 16px 0;
overflow-x: hidden;
overflow-y: auto;
background-color: rgb(249, 249, 249);
@@ -524,15 +519,15 @@ label {
background-color: #3a85ff;
}
.sidebar-nav > li .app-count, .sidebar-nav > li .worker-count {
font-size: 14px;
.sidebar-nav > li .app-count, .sidebar-nav > li .worker-count, .sidebar-nav > li .website-count {
font-size: 16px;
color: #6d767d;
font-weight: 400;
margin-left: 10px;
float: right;
}
.tab-btn.active .app-count, .tab-btn.active .worker-count{
.tab-btn.active .app-count, .tab-btn.active .worker-count, .tab-btn.active .website-count{
color: #f6f6f6;
}
@@ -753,7 +748,7 @@ th.sorted{
user-select: none !important;
}
.my-apps-title, .my-workers-title {
.my-apps-title, .my-workers-title, .my-websites-title {
font-size: 27px;
margin-top: 0px;
float: left;
@@ -952,18 +947,19 @@ ol li:before {
background-color: rgba(0, 0, 0, 0.4);
}
.create-an-app-btn{
float:right; margin-bottom: 10px;
.create-an-app-btn, .create-a-website-btn{
float:right;
margin-bottom: 10px;
}
.create-an-app-btn img{
.create-an-app-btn img, .create-a-website-btn img{
width: 20px;
margin-bottom: -5px;
margin-right: 10px;
}
.create-a-worker-btn{
.create-a-worker-btn, .create-a-website-btn{
float:right; margin-bottom: 10px;
}
.create-a-worker-btn img{
.create-a-worker-btn img, .create-a-website-btn img{
width: 20px;
margin-bottom: -5px;
margin-right: 5px;
@@ -976,7 +972,7 @@ ol li:before {
.ip-terms-notice{
font-size: 12px; margin-top: 20px; margin-bottom: 0;
}
.app-list-nav, .worker-list-nav{
.app-list-nav, .worker-list-nav, .website-list-nav{
overflow: hidden; margin-bottom: 40px; margin-top: 10px;
}
.back-to-main-btn{
@@ -989,7 +985,7 @@ ol li:before {
display: flex;
align-items: center;
}
.app-title{
.app-title, .website-title{
margin-top:0px;
margin-bottom: 0;
}
@@ -1346,7 +1342,7 @@ dialog{
background-color: #ededed;
}
.worker-checkbox, .app-checkbox {
.worker-checkbox, .app-checkbox, .website-checkbox {
width: 20px;
height: 20px;
}
+1 -1
View File
@@ -290,7 +290,6 @@
<div class="website-list-nav">
<h1 class="my-websites-title">My Websites<span class="website-count"></span></h1>
<button class="create-a-website-btn button button-primary"><img src="./img/website-white.svg"> New Website</button>
<button class="refresh-website-list button button-default" style="width:40px; padding: 10px; float: right; margin-right: 10px;" title="Refresh"><svg style="width: 18px; height: 18px; margin-top: -2px; display: block;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.1999/xlink" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32" stroke-width="2"><g stroke-width="2" transform="translate(0.5, 0.5)"><path data-cap="butt" d="M29.382,9.217A15,15,0,0,0,1,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline points="28.383 1.22 29.383 9.22 21.383 8.22" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline><path data-cap="butt" data-color="color-2" d="M2.618,22.783A15,15,0,0,0,31,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline data-color="color-2" points="3.617 30.78 2.617 22.78 10.617 23.78" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline></g></svg></button>
</div>
<div style="margin-bottom: 10px; position: relative; width: 300px; float:left;">
@@ -299,6 +298,7 @@
</div>
<button class="button button-danger disabled delete-websites-btn" style="float:right;">Delete</button>
<button class="refresh-website-list" style="width:40px; padding: 10px; float: right; margin-right: 10px;" title="Refresh"><svg style="width: 18px; height: 18px; margin-top: -2px; display: block;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.1999/xlink" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32" stroke-width="2"><g stroke-width="2" transform="translate(0.5, 0.5)"><path data-cap="butt" d="M29.382,9.217A15,15,0,0,0,1,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline points="28.383 1.22 29.383 9.22 21.383 8.22" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline><path data-cap="butt" data-color="color-2" d="M2.618,22.783A15,15,0,0,0,31,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline data-color="color-2" points="3.617 30.78 2.617 22.78 10.617 23.78" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline></g></svg></button>
<div style="overflow-x: auto; clear: both;">
<table class="table" id="website-list-table">
<thead class="disable-user-select">
+2 -1
View File
@@ -2601,12 +2601,13 @@ function app_context_menu(app_name, app_title, app_uid) {
puter.ui.contextMenu({
items: [
{
label: 'Open',
label: 'Open App',
type: 'primary',
action: () => {
puter.ui.launchApp(app_name);
},
},
'-',
{
label: 'Edit',
type: 'primary',
+7
View File
@@ -115,6 +115,13 @@ $(document).on('click', '.tab-btn', async function (e) {
activeTab = 'workers';
}
// ---------------------------------------------------------------
// Websites tab
// ---------------------------------------------------------------
else if ($(this).attr('data-tab') === 'websites') {
refresh_websites_list();
activeTab = 'websites';
}
// ---------------------------------------------------------------
// Payout Method tab
// ---------------------------------------------------------------
else if ($(this).attr('data-tab') === 'payout-method') {
+347 -2
View File
@@ -1,4 +1,349 @@
async function init_workers() {
let sortBy = 'created_at';
let sortDirection = 'desc';
window.websites = [];
let search_query;
window.create_website = async (name) => {
let website = await puter.hosting.create(name, window.default_website_file);
return website;
}
export default init_workers;
window.refresh_websites_list = async (show_loading = false) => {
if (show_loading)
puter.ui.showSpinner();
// puter.hosting.list() returns an array of website objects
window.websites = await puter.hosting.list();
// Get websites
if (window.activeTab === 'websites' && window.websites.length > 0) {
$('.website-card').remove();
$('#no-websites-notice').hide();
$('#website-list').show();
window.websites.forEach((website) => {
// append row to website-list-table
$('#website-list-table > tbody').append(generate_website_card(website));
});
} else {
$('#no-websites-notice').show();
$('#website-list').hide();
}
count_websites();
}
async function init_websites() {
}
$(document).on('click', '.create-a-website-btn', async function (e) {
let name = await puter.ui.prompt('Please enter a name for your website:', 'my-awesome-website');
if (name) {
await create_website(name);
}
})
$(document).on('change', '.website-checkbox', function (e) {
// determine if select-all checkbox should be checked, indeterminate, or unchecked
if ($('.website-checkbox:checked').length === $('.website-checkbox').length) {
$('.select-all-websites').prop('indeterminate', false);
$('.select-all-websites').prop('checked', true);
} else if ($('.website-checkbox:checked').length > 0) {
$('.select-all-websites').prop('indeterminate', true);
$('.select-all-websites').prop('checked', false);
}
else {
$('.select-all-websites').prop('indeterminate', false);
$('.select-all-websites').prop('checked', false);
}
// activate row
if ($(this).is(':checked'))
$(this).closest('tr').addClass('active');
else
$(this).closest('tr').removeClass('active');
// enable delete button if at least one checkbox is checked
if ($('.website-checkbox:checked').length > 0)
$('.delete-websites-btn').removeClass('disabled');
else
$('.delete-websites-btn').addClass('disabled');
})
$(document).on('change', '.select-all-websites', function (e) {
if ($(this).is(':checked')) {
$('.website-checkbox').prop('checked', true);
$('.website-card').addClass('active');
$('.delete-websites-btn').removeClass('disabled');
} else {
$('.website-checkbox').prop('checked', false);
$('.website-card').removeClass('active');
$('.delete-websites-btn').addClass('disabled');
}
})
$('.refresh-website-list').on('click', function (e) {
puter.ui.showSpinner();
refresh_website_list();
puter.ui.hideSpinner();
})
$('th.sort').on('click', function (e) {
// determine what column to sort by
const sortByColumn = $(this).attr('data-column');
// toggle sort direction
if (sortByColumn === sortBy) {
if (sortDirection === 'asc')
sortDirection = 'desc';
else
sortDirection = 'asc';
}
else {
sortBy = sortByColumn;
sortDirection = 'desc';
}
// update arrow
$('.sort-arrow').css('display', 'none');
$('#website-list-table').find('th').removeClass('sorted');
$(this).find('.sort-arrow-' + sortDirection).css('display', 'inline');
$(this).addClass('sorted');
sort_websites();
});
function sort_websites() {
let sorted_websites;
// sort
if (sortDirection === 'asc'){
sorted_websites = websites.sort((a, b) => {
if(sortBy === 'name'){
return a[sortBy].localeCompare(b[sortBy]);
}else if(sortBy === 'created_at'){
return new Date(a[sortBy]) - new Date(b[sortBy]);
} else if(sortBy === 'user_count' || sortBy === 'open_count'){
return a.stats[sortBy] - b.stats[sortBy];
}else{
a[sortBy] > b[sortBy] ? 1 : -1
}
});
}else{
sorted_websites = websites.sort((a, b) => {
if(sortBy === 'name'){
return b[sortBy].localeCompare(a[sortBy]);
}else if(sortBy === 'created_at'){
return new Date(b[sortBy]) - new Date(a[sortBy]);
} else if(sortBy === 'user_count' || sortBy === 'open_count'){
return b.stats[sortBy] - a.stats[sortBy];
}else{
b[sortBy] > a[sortBy] ? 1 : -1
}
});
}
// refresh website list
$('.website-card').remove();
sorted_websites.forEach(website => {
$('#website-list-table > tbody').append(generate_website_card(website));
});
count_websites();
// show websites that match search_query and hide websites that don't
if (search_query) {
// show websites that match search_query and hide websites that don't
websites.forEach((website) => {
if (website.name.toLowerCase().includes(search_query.toLowerCase())) {
$(`.website-card[data-name="${html_encode(website.name)}"]`).show();
} else {
$(`.website-card[data-name="${html_encode(website.name)}"]`).hide();
}
})
}
}
function count_websites() {
let count = 0;
$('.website-card').each(function () {
count++;
})
$('.website-count').html(count);
return count;
}
function generate_website_card(website) {
return `
<tr class="website-card" data-name="${html_encode(website.subdomain)}">
<td style="width:50px; vertical-align: middle; line-height: 1;">
<input type="checkbox" class="website-checkbox" data-website-name="${website.subdomain}">
</td>
<td style="font-family: monospace; font-size: 14px; vertical-align: middle;"><a href="https://${website.subdomain}.puter.site" target="_blank">${website.subdomain}.puter.site</a></td>
<td style="font-size: 14px; vertical-align: middle;">${website.created_at}</td>
<td style="vertical-align: middle;"><img class="options-icon options-icon-website" data-website-name="${website.subdomain}" src="./img/options.svg"></td>
</tr>
`;
}
$(document).on('input change keyup keypress keydown paste cut', '.search', function (e) {
// search apps for query
search_query = $(this).val().toLowerCase();
if (search_query === '') {
// hide 'clear search' button
$('.search-clear').hide();
// show all apps again
$(`.website-card`).show();
} else {
// show 'clear search' button
$('.search-clear').show();
// show apps that match search_query and hide apps that don't
websites.forEach((website) => {
if (
website.name.toLowerCase().includes(search_query.toLowerCase())
)
{
$(`.website-card[data-name="${website.name}"]`).show();
} else {
$(`.website-card[data-name="${website.name}"]`).hide();
}
})
}
})
$(document).on('click', '.delete-websites-btn', async function (e) {
// show confirmation alert
let resp = await puter.ui.alert(`Are you sure you want to delete the selected websites?`, [
{
label: 'Delete',
type: 'danger',
value: 'delete',
},
{
label: 'Cancel',
},
], {
type: 'warning',
});
if (resp === 'delete') {
// disable delete button
$('.delete-websites-btn').addClass('disabled');
// show 'deleting' modal
puter.ui.showSpinner();
let start_ts = Date.now();
const websites = $('.website-checkbox:checked').toArray();
// delete all checked websites
for (let website of websites) {
let website_name = $(website).attr('data-website-name');
// delete website
await puter.hosting.delete(website_name)
// remove website card
$(`.website-card[data-name="${website_name}"]`).fadeOut(200, function name(params) {
$(this).remove();
if ($(`.website-card`).length === 0) {
$('section:not(.sidebar)').hide();
$('#no-websites-notice').show();
} else {
$('section:not(.sidebar)').hide();
$('#website-list').show();
}
count_websites();
});
try{
count_websites();
} catch(err) {
console.log(err);
}
}
// close 'deleting' modal
setTimeout(() => {
puter.ui.hideSpinner();
if($('.website-checkbox:checked').length === 0){
// disable delete button
$('.delete-websites-btn').addClass('disabled');
// reset the 'select all' checkbox
$('.select-all-websites').prop('indeterminate', false);
$('.select-all-websites').prop('checked', false);
}
}, (start_ts - Date.now()) > 500 ? 0 : 500);
}
})
$(document).on('contextmenu', '.website-card', function (e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
puter.ui.contextMenu({
items: [
{
label: 'Delete',
type: 'danger',
action: () => {
attempt_delete_website($(this).attr('data-name'));
},
},
],
});
})
$(document).on('click', '.options-icon-website', function (e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
puter.ui.contextMenu({
items: [
{
label: 'Delete',
type: 'danger',
action: () => {
attempt_delete_website($(this).attr('data-website-name'));
},
},
],
});
})
async function attempt_delete_website(website_name) {
// confirm delete
const alert_resp = await puter.ui.alert(`Are you sure you want to premanently delete <strong>${html_encode(website_name)}.puter.site</strong>?`,
[
{
label: 'Yes, delete permanently',
value: 'delete',
type: 'danger',
},
{
label: 'Cancel'
},
]
);
if (alert_resp === 'delete') {
// remove website card and update website count
$(`.website-card[data-name="${website_name}"]`).fadeOut(200, function name(params) {
$(this).remove();
if ($(`.website-card`).length === 0) {
$('section:not(.sidebar)').hide();
$('#no-websites-notice').show();
} else {
$('section:not(.sidebar)').hide();
$('#website-list').show();
}
count_websites();
});
// delete website
puter.hosting.delete(website_name);
}
}
export default init_websites;
+1 -1
View File
@@ -205,7 +205,7 @@ function count_workers() {
function generate_worker_card(worker) {
return `
<tr class="worker-card" data-name="${html_encode(worker.name)}">
<td style="width:50px; vertical-align: middle;">
<td style="width:50px; vertical-align: middle; line-height: 1;">
<input type="checkbox" class="worker-checkbox" data-worker-name="${worker.name}">
</td>
<td style="font-family: monospace; font-size: 14px; vertical-align: middle;">${worker.name}</td>