diff --git a/src/dev-center/css/style.css b/src/dev-center/css/style.css
index b37c23d03..0b90ffff6 100644
--- a/src/dev-center/css/style.css
+++ b/src/dev-center/css/style.css
@@ -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;
}
diff --git a/src/dev-center/index.html b/src/dev-center/index.html
index 7a2a752ae..a87c52662 100644
--- a/src/dev-center/index.html
+++ b/src/dev-center/index.html
@@ -290,7 +290,6 @@
My Websites
-
@@ -299,6 +298,7 @@
+
diff --git a/src/dev-center/js/apps.js b/src/dev-center/js/apps.js
index f30799c03..68ab0ca40 100644
--- a/src/dev-center/js/apps.js
+++ b/src/dev-center/js/apps.js
@@ -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',
diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js
index c25cfb59c..7eacdc8dd 100644
--- a/src/dev-center/js/dev-center.js
+++ b/src/dev-center/js/dev-center.js
@@ -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') {
diff --git a/src/dev-center/js/websites.js b/src/dev-center/js/websites.js
index 0bb9f1062..c7f0d052b 100644
--- a/src/dev-center/js/websites.js
+++ b/src/dev-center/js/websites.js
@@ -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;
\ No newline at end of file
+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 `
+
+ |
+
+ |
+ ${website.subdomain}.puter.site |
+ ${website.created_at} |
+  |
+
+ `;
+}
+
+$(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 ${html_encode(website_name)}.puter.site?`,
+ [
+ {
+ 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;
\ No newline at end of file
diff --git a/src/dev-center/js/workers.js b/src/dev-center/js/workers.js
index c2964f2ea..bd244dc1a 100644
--- a/src/dev-center/js/workers.js
+++ b/src/dev-center/js/workers.js
@@ -205,7 +205,7 @@ function count_workers() {
function generate_worker_card(worker) {
return `
- |
+ |
|
${worker.name} |