mirror of
https://github.com/HeyPuter/puter.git
synced 2025-12-30 17:50:00 -06:00
Dashboard (#2104)
* Add dashboard UI and routing support Introduces a new Dashboard UI component with sidebar navigation and user options. Updates backend routing to redirect /dashboard to the root path. Integrates dashboard mode detection and initialization in the GUI, including responsive styles and logic to open the dashboard or desktop as appropriate. * Improve dashboard user menu and UI behavior Added support for multiple logged-in users, session saving for temporary users, and improved context menu options in the dashboard. Updated CSS for user button state and adjusted font size for signup terms. Changed dashboard initialization to use UIDashboard instead of UIWindow. * Add dynamic apps section to dashboard UI * Add specific class for dashboard apps section * Refactor dashboard tabs into modular components * Update style.css * Add Developers menu and improve signup window behavior Added a 'Developers' menu item to the dashboard linking to developer.puter.com. Updated login and signup window logic to ensure the signup window opens centered and dominant, improving user experience when transitioning from login to signup. * Update UIWindowLogin.js * Refactor apps tab UI and improve app card interaction
This commit is contained in:
@@ -346,6 +346,10 @@ router.all('*', async function (req, res, next) {
|
||||
else if ( path.startsWith('/settings') ) {
|
||||
path = '/';
|
||||
}
|
||||
// /dashboard
|
||||
else if ( path === '/dashboard' || path === '/dashboard/' ) {
|
||||
path = '/';
|
||||
}
|
||||
// /app/
|
||||
else if ( path.startsWith('/app/') ) {
|
||||
app_name = path.replace('/app/', '');
|
||||
|
||||
99
src/gui/src/UI/Dashboard/TabApps.js
Normal file
99
src/gui/src/UI/Dashboard/TabApps.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function buildAppsSection () {
|
||||
let apps_str = '';
|
||||
if ( window.launch_apps?.recommended?.length > 0 ) {
|
||||
apps_str += '<div class="dashboard-apps-grid">';
|
||||
for ( let index = 0; index < window.launch_apps.recommended.length; index++ ) {
|
||||
const app_info = window.launch_apps.recommended[index];
|
||||
apps_str += `<div title="${html_encode(app_info.title)}" data-name="${html_encode(app_info.name)}" class="dashboard-app-card start-app-card">`;
|
||||
apps_str += `<div class="start-app" data-app-name="${html_encode(app_info.name)}" data-app-uuid="${html_encode(app_info.uuid)}" data-app-icon="${html_encode(app_info.icon)}" data-app-title="${html_encode(app_info.title)}">`;
|
||||
apps_str += `<img class="dashboard-app-icon" src="${html_encode(app_info.icon ? app_info.icon : window.icons['app.svg'])}">`;
|
||||
apps_str += `<span class="dashboard-app-title">${html_encode(app_info.title)}</span>`;
|
||||
apps_str += '</div>';
|
||||
apps_str += '</div>';
|
||||
}
|
||||
apps_str += '</div>';
|
||||
}
|
||||
|
||||
// No apps message
|
||||
if ( (!window.launch_apps?.recent || window.launch_apps.recent.length === 0) &&
|
||||
(!window.launch_apps?.recommended || window.launch_apps.recommended.length === 0) ) {
|
||||
apps_str += '<p class="dashboard-no-apps">No apps available yet.</p>';
|
||||
}
|
||||
|
||||
return apps_str;
|
||||
}
|
||||
|
||||
const TabApps = {
|
||||
id: 'apps',
|
||||
label: 'My Apps',
|
||||
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>`,
|
||||
|
||||
html () {
|
||||
return '<div class="dashboard-apps-container"></div>';
|
||||
},
|
||||
|
||||
init ($el_window) {
|
||||
// Load apps initially
|
||||
this.loadApps($el_window);
|
||||
|
||||
// Handle app clicks - open in new browser tab
|
||||
$el_window.on('click', '.dashboard-apps-container .start-app', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const appName = $(this).attr('data-app-name');
|
||||
if ( appName ) {
|
||||
const appUrl = `/app/${appName}`;
|
||||
window.open(appUrl, '_blank');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async loadApps ($el_window) {
|
||||
// If launch_apps is not populated yet, fetch from server
|
||||
if ( !window.launch_apps || !window.launch_apps.recent || window.launch_apps.recent.length === 0 ) {
|
||||
try {
|
||||
window.launch_apps = await $.ajax({
|
||||
url: `${window.api_origin}/get-launch-apps?icon_size=64`,
|
||||
type: 'GET',
|
||||
async: true,
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.auth_token}`,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load launch apps:', e);
|
||||
}
|
||||
}
|
||||
// Populate the apps container
|
||||
$el_window.find('.dashboard-apps-container').html(buildAppsSection());
|
||||
},
|
||||
|
||||
onActivate ($el_window) {
|
||||
// Refresh apps when navigating to apps section
|
||||
this.loadApps($el_window);
|
||||
},
|
||||
};
|
||||
|
||||
export default TabApps;
|
||||
|
||||
38
src/gui/src/UI/Dashboard/TabFiles.js
Normal file
38
src/gui/src/UI/Dashboard/TabFiles.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const TabFiles = {
|
||||
id: 'files',
|
||||
label: 'My Files',
|
||||
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
|
||||
|
||||
html () {
|
||||
let h = '';
|
||||
h += '<h2>My Files</h2>';
|
||||
h += '<p>Your files will appear here.</p>';
|
||||
return h;
|
||||
},
|
||||
|
||||
init ($el_window) {
|
||||
// Files tab initialization logic can go here
|
||||
},
|
||||
};
|
||||
|
||||
export default TabFiles;
|
||||
|
||||
281
src/gui/src/UI/Dashboard/UIDashboard.js
Normal file
281
src/gui/src/UI/Dashboard/UIDashboard.js
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import UIWindow from '../UIWindow.js';
|
||||
import UIContextMenu from '../UIContextMenu.js';
|
||||
import UIWindowSettings from '../Settings/UIWindowSettings.js';
|
||||
import UIAlert from '../UIAlert.js';
|
||||
import UIWindowSaveAccount from '../UIWindowSaveAccount.js';
|
||||
import UIWindowLogin from '../UIWindowLogin.js';
|
||||
import UIWindowFeedback from '../UIWindowFeedback.js';
|
||||
|
||||
// Import tab modules
|
||||
import TabFiles from './TabFiles.js';
|
||||
import TabApps from './TabApps.js';
|
||||
|
||||
// Registry of all available tabs
|
||||
const tabs = [
|
||||
TabFiles,
|
||||
TabApps,
|
||||
];
|
||||
|
||||
async function UIDashboard (options) {
|
||||
options = options ?? {};
|
||||
|
||||
let h = '';
|
||||
|
||||
h += '<div class="dashboard">';
|
||||
|
||||
// Mobile sidebar toggle
|
||||
h += '<button class="dashboard-sidebar-toggle">';
|
||||
h += '<span></span><span></span><span></span>';
|
||||
h += '</button>';
|
||||
|
||||
// Sidebar
|
||||
h += '<div class="dashboard-sidebar">';
|
||||
// Navigation items container
|
||||
h += '<div class="dashboard-sidebar-nav">';
|
||||
for ( let i = 0; i < tabs.length; i++ ) {
|
||||
const tab = tabs[i];
|
||||
const isActive = i === 0 ? ' active' : '';
|
||||
h += `<div class="dashboard-sidebar-item${isActive}" data-section="${tab.id}">`;
|
||||
h += tab.icon;
|
||||
h += tab.label;
|
||||
h += '</div>';
|
||||
}
|
||||
h += '</div>';
|
||||
|
||||
// User options button at bottom
|
||||
h += '<div class="dashboard-user-options">';
|
||||
h += `<div class="dashboard-user-btn">`;
|
||||
h += `<div class="dashboard-user-avatar" style="background-image: url(${window.user?.profile?.picture || window.icons['profile.svg']})"></div>`;
|
||||
h += `<span class="dashboard-user-name">${html_encode(window.user?.username || 'User')}</span>`;
|
||||
h += `<svg class="dashboard-user-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>`;
|
||||
h += `</div>`;
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
|
||||
// Main content area
|
||||
h += '<div class="dashboard-content">';
|
||||
for ( let i = 0; i < tabs.length; i++ ) {
|
||||
const tab = tabs[i];
|
||||
const isActive = i === 0 ? ' active' : '';
|
||||
h += `<div class="dashboard-section dashboard-section-${tab.id}${isActive}" data-section="${tab.id}">`;
|
||||
h += tab.html();
|
||||
h += '</div>';
|
||||
}
|
||||
h += '</div>';
|
||||
|
||||
h += '</div>';
|
||||
|
||||
const el_window = await UIWindow({
|
||||
title: 'Dashboard',
|
||||
app: 'dashboard',
|
||||
single_instance: true,
|
||||
is_fullpage: true,
|
||||
is_resizable: false,
|
||||
is_maximized: true,
|
||||
has_head: false,
|
||||
body_content: h,
|
||||
window_class: 'window-dashboard',
|
||||
body_css: {
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
const $el_window = $(el_window);
|
||||
|
||||
// Initialize all tabs
|
||||
for ( const tab of tabs ) {
|
||||
if ( tab.init ) {
|
||||
tab.init($el_window);
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar item click handler
|
||||
$el_window.on('click', '.dashboard-sidebar-item', function () {
|
||||
const $this = $(this);
|
||||
const section = $this.attr('data-section');
|
||||
|
||||
// Update active sidebar item
|
||||
$el_window.find('.dashboard-sidebar-item').removeClass('active');
|
||||
$this.addClass('active');
|
||||
|
||||
// Update active content section
|
||||
$el_window.find('.dashboard-section').removeClass('active');
|
||||
$el_window.find(`.dashboard-section[data-section="${section}"]`).addClass('active');
|
||||
|
||||
// Call onActivate for the tab if it exists
|
||||
const tab = tabs.find(t => t.id === section);
|
||||
if ( tab && tab.onActivate ) {
|
||||
tab.onActivate($el_window);
|
||||
}
|
||||
|
||||
// Close sidebar on mobile after selection
|
||||
$el_window.find('.dashboard-sidebar').removeClass('open');
|
||||
$el_window.find('.dashboard-sidebar-toggle').removeClass('open');
|
||||
});
|
||||
|
||||
// Mobile toggle handler
|
||||
$el_window.on('click', '.dashboard-sidebar-toggle', function () {
|
||||
$(this).toggleClass('open');
|
||||
$el_window.find('.dashboard-sidebar').toggleClass('open');
|
||||
});
|
||||
|
||||
// User options button click handler
|
||||
$el_window.on('click', '.dashboard-user-btn', function (e) {
|
||||
const $btn = $(this);
|
||||
const pos = this.getBoundingClientRect();
|
||||
|
||||
// Don't open if already open
|
||||
if ($('.context-menu[data-id="dashboard-user-menu"]').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items = [];
|
||||
|
||||
// Save Session (if temp user)
|
||||
if (window.user.is_temp) {
|
||||
items.push({
|
||||
html: i18n('save_session'),
|
||||
icon: '<svg style="margin-bottom: -4px; width: 16px; height: 16px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="M45.521,39.04L27.527,5.134c-1.021-1.948-3.427-2.699-5.375-1.679-.717,.376-1.303,.961-1.679,1.679L2.479,39.04c-.676,1.264-.635,2.791,.108,4.017,.716,1.207,2.017,1.946,3.42,1.943H41.993c1.403,.003,2.704-.736,3.42-1.943,.743-1.226,.784-2.753,.108-4.017ZM23.032,15h1.937c.565,0,1.017,.467,1,1.031l-.438,14c-.017,.54-.459,.969-1,.969h-1.062c-.54,0-.983-.429-1-.969l-.438-14c-.018-.564,.435-1.031,1-1.031Zm.968,25c-1.657,0-3-1.343-3-3s1.343-3,3-3,3,1.343,3,3-1.343,3-3,3Z" fill="#ffbb00"/></svg>',
|
||||
onClick: async function () {
|
||||
UIWindowSaveAccount({
|
||||
send_confirmation_code: false,
|
||||
default_username: window.user.username,
|
||||
});
|
||||
},
|
||||
});
|
||||
items.push('-');
|
||||
}
|
||||
|
||||
// Logged in users
|
||||
if (window.logged_in_users.length > 0) {
|
||||
let users_arr = window.logged_in_users;
|
||||
|
||||
// bring logged in user's item to top
|
||||
users_arr.sort(function (x, y) {
|
||||
return x.uuid === window.user.uuid ? -1 : y.uuid == window.user.uuid ? 1 : 0;
|
||||
});
|
||||
|
||||
// create menu items for each user
|
||||
users_arr.forEach(l_user => {
|
||||
items.push({
|
||||
html: l_user.username,
|
||||
icon: l_user.username === window.user.username ? '✓' : '',
|
||||
onClick: async function () {
|
||||
if (l_user.username === window.user.username) {
|
||||
return;
|
||||
}
|
||||
window.update_auth_data(l_user.auth_token, l_user);
|
||||
location.reload();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
items.push('-');
|
||||
|
||||
items.push({
|
||||
html: i18n('add_existing_account'),
|
||||
onClick: async function () {
|
||||
await UIWindowLogin({
|
||||
reload_on_success: true,
|
||||
send_confirmation_code: false,
|
||||
window_options: {
|
||||
has_head: true,
|
||||
stay_on_top: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
items.push('-');
|
||||
}
|
||||
|
||||
// Build final menu items
|
||||
const menuItems = [
|
||||
...items,
|
||||
// Settings
|
||||
{
|
||||
html: i18n('settings'),
|
||||
onClick: async function () {
|
||||
UIWindowSettings();
|
||||
},
|
||||
},
|
||||
// Developer
|
||||
{
|
||||
html: 'Developers<svg style="width: 11px; height: 11px; margin-left:2px; margin-bottom:-1px;" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m26 28h-20a2.0027 2.0027 0 0 1 -2-2v-20a2.0027 2.0027 0 0 1 2-2h10v2h-10v20h20v-10h2v10a2.0027 2.0027 0 0 1 -2 2z"/><path d="m20 2v2h6.586l-8.586 8.586 1.414 1.414 8.586-8.586v6.586h2v-10z"/><path d="m0 0h32v32h-32z" fill="none"/></svg>',
|
||||
html_active: 'Developers<svg style="width: 11px; height: 11px; margin-left:2px; margin-bottom:-1px;" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m26 28h-20a2.0027 2.0027 0 0 1 -2-2v-20a2.0027 2.0027 0 0 1 2-2h10v2h-10v20h20v-10h2v10a2.0027 2.0027 0 0 1 -2 2z" style="fill: rgb(255, 255, 255);"/><path d="m20 2v2h6.586l-8.586 8.586 1.414 1.414 8.586-8.586v6.586h2v-10z" style="fill: rgb(255, 255, 255);"/><path d="m0 0h32v32h-32z" fill="none"/></svg>',
|
||||
onClick: function () {
|
||||
window.open('https://developer.puter.com', '_blank');
|
||||
},
|
||||
},
|
||||
// Contact Us
|
||||
{
|
||||
html: i18n('contact_us'),
|
||||
onClick: async function () {
|
||||
UIWindowFeedback();
|
||||
},
|
||||
},
|
||||
'-',
|
||||
// Log out
|
||||
{
|
||||
html: i18n('log_out'),
|
||||
onClick: async function () {
|
||||
// Check for open windows
|
||||
if ($('.window-app').length > 0) {
|
||||
const alert_resp = await UIAlert({
|
||||
message: `<p>${i18n('confirm_open_apps_log_out')}</p>`,
|
||||
buttons: [
|
||||
{
|
||||
label: i18n('close_all_windows_and_log_out'),
|
||||
value: 'close_and_log_out',
|
||||
type: 'primary',
|
||||
},
|
||||
{
|
||||
label: i18n('cancel'),
|
||||
},
|
||||
],
|
||||
});
|
||||
if (alert_resp === 'close_and_log_out') {
|
||||
window.logout();
|
||||
}
|
||||
} else {
|
||||
window.logout();
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
UIContextMenu({
|
||||
id: 'dashboard-user-menu',
|
||||
parent_element: $btn[0],
|
||||
position: {
|
||||
top: pos.top - 8,
|
||||
left: pos.left
|
||||
},
|
||||
items: menuItems
|
||||
});
|
||||
});
|
||||
|
||||
return el_window;
|
||||
}
|
||||
|
||||
export default UIDashboard;
|
||||
@@ -44,6 +44,7 @@ function UIWindowSignup (options) {
|
||||
|
||||
// Form
|
||||
h += '<div style="padding: 20px; border-bottom: 1px solid #ced7e1;">';
|
||||
|
||||
// title
|
||||
h += `<h1 class="signup-form-title">${i18n('create_free_account')}</h1>`;
|
||||
// signup form
|
||||
@@ -122,7 +123,7 @@ function UIWindowSignup (options) {
|
||||
allow_native_ctxmenu: true,
|
||||
allow_user_select: true,
|
||||
...options.window_options,
|
||||
dominant: false,
|
||||
dominant: true,
|
||||
center: true,
|
||||
onAppend: function (el_window) {
|
||||
$(el_window).find('.username').get(0).focus({ preventScroll: true });
|
||||
|
||||
@@ -4017,7 +4017,7 @@ fieldset[name=number-code] {
|
||||
}
|
||||
|
||||
.signup-terms {
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-top: 10px;
|
||||
bottom: 10px;
|
||||
@@ -5436,4 +5436,300 @@ fieldset[name=number-code] {
|
||||
.update-usage-details svg{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* ======================================
|
||||
Dashboard
|
||||
====================================== */
|
||||
|
||||
.dashboard {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.dashboard-sidebar {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
background: #f5f5f5;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding: 16px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboard-sidebar-nav {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dashboard-sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #444;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.dashboard-sidebar-item svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dashboard-sidebar-item:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.dashboard-sidebar-item.active {
|
||||
background: #e0e0e0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* User options button at bottom of sidebar */
|
||||
.dashboard-user-options {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dashboard-user-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.dashboard-user-btn:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.dashboard-user-btn.has-open-contextmenu {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.dashboard-user-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-color: #ddd;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dashboard-user-name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-user-chevron {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #888;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
flex: 1;
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dashboard-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dashboard-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dashboard-section h2 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dashboard-section p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dashboard-section-apps {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Dashboard Apps */
|
||||
.dashboard-apps-container {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dashboard-apps-heading {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.dashboard-apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dashboard-app-card {
|
||||
width: 100%;
|
||||
transition: background-color 0.15s, transform 0.1s;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.dashboard-app-card .start-app {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dashboard-app-card:hover {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.dashboard-app-card:hover .start-app, .dashboard-app-card .start-app:hover {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.dashboard-app-card:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.dashboard-app-card .start-app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard-app-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 8px;
|
||||
filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.dashboard-app-card .start-app{
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.dashboard-app-card .start-app:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.dashboard-app-title {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.dashboard-no-apps {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
/* Mobile sidebar toggle */
|
||||
.dashboard-sidebar-toggle {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 100;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dashboard-sidebar-toggle span {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 2px;
|
||||
background: #444;
|
||||
border-radius: 1px;
|
||||
transition: transform 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
.dashboard-sidebar-toggle.open span:nth-child(1) {
|
||||
transform: rotate(45deg) translate(4px, 4px);
|
||||
}
|
||||
|
||||
.dashboard-sidebar-toggle.open span:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dashboard-sidebar-toggle.open span:nth-child(3) {
|
||||
transform: rotate(-45deg) translate(4px, -4px);
|
||||
}
|
||||
|
||||
/* Responsive: tablet and below */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-sidebar-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dashboard-sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
z-index: 99;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
padding: 64px 16px 24px;
|
||||
}
|
||||
|
||||
.dashboard-apps-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dashboard-app-card {
|
||||
padding: 10px 6px;
|
||||
}
|
||||
|
||||
.dashboard-app-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import { ProcessService } from './services/ProcessService.js';
|
||||
import { SettingsService } from './services/SettingsService.js';
|
||||
import { ThemeService } from './services/ThemeService.js';
|
||||
import { privacy_aware_path } from './util/desktop.js';
|
||||
import UIDashboard from './UI/Dashboard/UIDashboard.js';
|
||||
|
||||
const launch_services = async function (options) {
|
||||
// === Services Data Structures ===
|
||||
@@ -150,6 +151,11 @@ if ( jQuery ) {
|
||||
};
|
||||
}
|
||||
|
||||
// are we in dashboard mode?
|
||||
if(window.location.pathname === '/dashboard' || window.location.pathname === '/dashboard/'){
|
||||
window.is_dashboard_mode = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a Turnstile challenge modal for first-time temp user creation
|
||||
* @param {Object} options - Configuration options
|
||||
@@ -348,6 +354,8 @@ window.initgui = async function (options) {
|
||||
|
||||
// Puter is in fullpage mode.
|
||||
window.is_fullpage_mode = true;
|
||||
} else if (window.is_dashboard_mode) {
|
||||
window.is_fullpage_mode = true;
|
||||
}
|
||||
|
||||
// Launch services before any UI is rendered
|
||||
@@ -601,15 +609,21 @@ window.initgui = async function (options) {
|
||||
window.update_auth_data(whoami.token || window.auth_token, whoami);
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Load desktop, only if we're not embedded in a popup
|
||||
// Load desktop, only if we're not embedded in a popup and not on the dashboard page
|
||||
// -------------------------------------------------------------------------------------
|
||||
if ( ! window.embedded_in_popup ) {
|
||||
if ( ! window.embedded_in_popup && ! window.is_dashboard_mode ) {
|
||||
await window.get_auto_arrange_data();
|
||||
puter.fs.stat({ path: window.desktop_path, consistency: 'eventual' }).then(desktop_fsentry => {
|
||||
UIDesktop({ desktop_fsentry: desktop_fsentry });
|
||||
});
|
||||
}
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Dashboard mode
|
||||
// -------------------------------------------------------------------------------------
|
||||
else if ( window.is_dashboard_mode ) {
|
||||
UIDashboard();
|
||||
}
|
||||
// -------------------------------------------------------------------------------------
|
||||
// If embedded in a popup, send the token to the opener and close the popup
|
||||
// -------------------------------------------------------------------------------------
|
||||
else {
|
||||
@@ -1107,15 +1121,21 @@ window.initgui = async function (options) {
|
||||
$('.window').close();
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Load desktop, if not embedded in a popup
|
||||
// Load desktop, if not embedded in a popup and not on the dashboard page
|
||||
// -------------------------------------------------------------------------------------
|
||||
if ( ! window.embedded_in_popup ) {
|
||||
if ( ! window.embedded_in_popup && ! window.is_dashboard_mode ) {
|
||||
await window.get_auto_arrange_data();
|
||||
puter.fs.stat({ path: window.desktop_path, consistency: 'eventual' }).then(desktop_fsentry => {
|
||||
UIDesktop({ desktop_fsentry: desktop_fsentry });
|
||||
});
|
||||
}
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Dashboard mode: open explorer pointing to home directory
|
||||
// -------------------------------------------------------------------------------------
|
||||
else if ( window.is_dashboard_mode ) {
|
||||
UIDashboard();
|
||||
}
|
||||
// -------------------------------------------------------------------------------------
|
||||
// If embedded in a popup, send the 'ready' event to referrer and close the popup
|
||||
// -------------------------------------------------------------------------------------
|
||||
else {
|
||||
|
||||
Reference in New Issue
Block a user