diff --git a/src/backend/src/routers/_default.js b/src/backend/src/routers/_default.js index 6401f84c..072ec2ce 100644 --- a/src/backend/src/routers/_default.js +++ b/src/backend/src/routers/_default.js @@ -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/', ''); diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js new file mode 100644 index 00000000..4a46bac9 --- /dev/null +++ b/src/gui/src/UI/Dashboard/TabApps.js @@ -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 . + */ + +function buildAppsSection () { + let apps_str = ''; + if ( window.launch_apps?.recommended?.length > 0 ) { + apps_str += '
'; + for ( let index = 0; index < window.launch_apps.recommended.length; index++ ) { + const app_info = window.launch_apps.recommended[index]; + apps_str += `
`; + apps_str += `
`; + apps_str += ``; + apps_str += `${html_encode(app_info.title)}`; + apps_str += '
'; + apps_str += '
'; + } + apps_str += '
'; + } + + // 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 += '

No apps available yet.

'; + } + + return apps_str; +} + +const TabApps = { + id: 'apps', + label: 'My Apps', + icon: ``, + + html () { + return '
'; + }, + + 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; + diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js new file mode 100644 index 00000000..9cdcad1c --- /dev/null +++ b/src/gui/src/UI/Dashboard/TabFiles.js @@ -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 . + */ + +const TabFiles = { + id: 'files', + label: 'My Files', + icon: ``, + + html () { + let h = ''; + h += '

My Files

'; + h += '

Your files will appear here.

'; + return h; + }, + + init ($el_window) { + // Files tab initialization logic can go here + }, +}; + +export default TabFiles; + diff --git a/src/gui/src/UI/Dashboard/UIDashboard.js b/src/gui/src/UI/Dashboard/UIDashboard.js new file mode 100644 index 00000000..021be251 --- /dev/null +++ b/src/gui/src/UI/Dashboard/UIDashboard.js @@ -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 . + */ + +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 += '
'; + + // Mobile sidebar toggle + h += ''; + + // Sidebar + h += '
'; + // Navigation items container + h += '
'; + for ( let i = 0; i < tabs.length; i++ ) { + const tab = tabs[i]; + const isActive = i === 0 ? ' active' : ''; + h += `
`; + h += tab.icon; + h += tab.label; + h += '
'; + } + h += '
'; + + // User options button at bottom + h += '
'; + h += `
`; + h += `
`; + h += `${html_encode(window.user?.username || 'User')}`; + h += ``; + h += `
`; + h += '
'; + h += '
'; + + // Main content area + h += '
'; + for ( let i = 0; i < tabs.length; i++ ) { + const tab = tabs[i]; + const isActive = i === 0 ? ' active' : ''; + h += `
`; + h += tab.html(); + h += '
'; + } + h += '
'; + + h += '
'; + + 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: '', + 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', + html_active: 'Developers', + 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: `

${i18n('confirm_open_apps_log_out')}

`, + 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; diff --git a/src/gui/src/UI/UIWindowSignup.js b/src/gui/src/UI/UIWindowSignup.js index 46ca9a03..d69d60ab 100644 --- a/src/gui/src/UI/UIWindowSignup.js +++ b/src/gui/src/UI/UIWindowSignup.js @@ -44,6 +44,7 @@ function UIWindowSignup (options) { // Form h += '
'; + // title h += `

${i18n('create_free_account')}

`; // 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 }); diff --git a/src/gui/src/css/style.css b/src/gui/src/css/style.css index 88d6fabe..9e9e2f5e 100644 --- a/src/gui/src/css/style.css +++ b/src/gui/src/css/style.css @@ -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; + } } \ No newline at end of file diff --git a/src/gui/src/initgui.js b/src/gui/src/initgui.js index db47f742..aadca0b9 100644 --- a/src/gui/src/initgui.js +++ b/src/gui/src/initgui.js @@ -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 {