dashboard backbone and scss work

This commit is contained in:
Matt Beaudoin
2025-11-08 14:31:55 -08:00
parent ed4f2181d5
commit dfefc6805e
15 changed files with 512 additions and 50 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
VITE_API_URL=http://localhost:3000/api
VITE_MOCK_AUTH=true

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
# API base URL for the backend that wraps the abra CLI
VITE_API_URL=http://localhost:3000/api
# Set to 'true' to bypass authentication for development
VITE_MOCK_AUTH=true

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>coopcloud-frontend</title>
</head>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 163 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,31 @@
@font-face {
font-family: 'Lora';
src: url('https://coopcloud.tech/font/Lora-Italic.woff2') format('woff2');
font-style: italic;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'Manrope';
src: url('https://coopcloud.tech/font/manrope.light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Manrope';
src: url('https://coopcloud.tech/font/manrope.medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Manrope';
src: url('https://coopcloud.tech/font/manrope.bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}

View File

@ -1,17 +1,18 @@
// Design tokens - colors, spacing, typography
// Colors
$primary: #667eea;
$primary-dark: #764ba2;
$secondary: #f093fb;
$primary: #EFEFEF;
$primary-dark: #ff4f88;
$secondary: #363636;
$success: #10b981;
$warning: #f59e0b;
$error: #ef4444;
$info: #3b82f6;
$text-primary: #333;
$text-secondary: #666;
$text-primary: #363636;
$text-secondary: #4a4a4a
;
$text-muted: #999;
$bg-primary: #ffffff;
@ -30,9 +31,12 @@ $spacing-2xl: 3rem;
$spacing-3xl: 4rem;
// Typography
$font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
$font-family-body: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI',
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif;
$font-family-heading: 'Lora', serif;
$font-size-xs: 0.75rem;
$font-size-sm: 0.875rem;

View File

@ -1,6 +1,7 @@
// 1. Configuration & helpers (no CSS output)
@use 'variables';
@use 'mixins';
@use 'fonts';
// 2. Global base styles
@use 'global';
@use 'global';

View File

@ -1,7 +1,7 @@
// components/HomePage.tsx
import React, { useEffect, useState } from 'react';
import { Layout } from './layout/Layout.tsx';
import { Layout } from './layout/Layout';
import { apiService } from '../services/api';
import type { AbraApp, AbraServer } from '../types';
// import './HomePage.scss';
@ -12,15 +12,29 @@ const HomePage: React.FC = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const isMockMode = import.meta.env.VITE_MOCK_AUTH === 'true';
useEffect(() => {
const fetchData = async () => {
try {
const [appsData, serversData] = await Promise.all([
apiService.getApps(),
apiService.getServers(),
]);
setApps(appsData);
setServers(serversData);
if (isMockMode) {
// Use mock API in development
const { mockApiService } = await import('../services/mockApi');
const [appsData, serversData] = await Promise.all([
mockApiService.getApps(),
mockApiService.getServers(),
]);
setApps(appsData);
setServers(serversData);
} else {
// Use real API in production
const [appsData, serversData] = await Promise.all([
apiService.getApps(),
apiService.getServers(),
]);
setApps(appsData);
setServers(serversData);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load data');
} finally {
@ -29,7 +43,7 @@ const HomePage: React.FC = () => {
};
fetchData();
}, []);
}, [isMockMode]);
if (loading) {
return (

View File

@ -67,13 +67,13 @@
}
.error-message {
background-color: rgba($error, 0.1);
color: darken($error, 10%);
// background-color: rgba($error, 0.1);
// color: darken($error, 10%);
padding: $spacing-md;
border-radius: $radius-md;
margin-bottom: $spacing-md;
font-size: $font-size-sm;
border-left: 4px solid $error;
// border-left: 4px solid $error;
}
.login-button {

View File

@ -1,9 +1,9 @@
// components/layout/Layout.tsx
import React from 'react';
// import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
// import './Layout.scss';
import './_layout.scss';
interface LayoutProps {
children: React.ReactNode;
@ -22,7 +22,7 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
<div className="layout">
<header className="layout-header">
<div className="header-content">
<h1 className="logo">Coop Cloud</h1>
<h1 onClick={() => navigate('/dashboard')} className="logo">Coop Cloud</h1>
<nav className="nav">
<button onClick={() => navigate('/dashboard')} className="nav-link">

View File

@ -0,0 +1,156 @@
// components/layout/Layout.scss
@use '../../assets/scss/variables' as *;
@use '../../assets/scss/mixins' as *;
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: $bg-secondary;
}
.layout-header {
@include gradient-primary;
color: $text-primary;
padding: $spacing-lg 0;
box-shadow: $shadow-lg;
.header-content {
max-width: 1400px;
margin: 0 auto;
padding: 0 $spacing-xl;
display: flex;
justify-content: space-between;
align-items: center;
gap: $spacing-xl;
@media (max-width: 768px) {
flex-direction: column;
gap: $spacing-md;
}
}
.logo {
margin: 0;
font-size: $font-size-3xl;
font-weight: $font-weight-bold;
letter-spacing: -0.5px;
@media (max-width: 768px) {
font-size: $font-size-2xl;
}
}
.nav {
display: flex;
gap: $spacing-sm;
flex: 1;
@media (max-width: 768px) {
width: 100%;
justify-content: center;
}
}
.nav-link {
background: none;
border: none;
color: $text-primary;
padding: $spacing-sm $spacing-lg;
cursor: pointer;
border-radius: $radius-md;
transition: all $transition-base;
font-size: $font-size-base;
font-weight: $font-weight-medium;
position: relative;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
// Active indicator
&.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 2px;
background-color: $bg-primary;
}
@media (max-width: 768px) {
padding: $spacing-sm $spacing-md;
font-size: $font-size-sm;
}
}
.user-menu {
display: flex;
align-items: center;
gap: $spacing-lg;
@media (max-width: 768px) {
width: 100%;
justify-content: space-between;
}
.username {
font-weight: $font-weight-medium;
font-size: $font-size-base;
@media (max-width: 480px) {
font-size: $font-size-sm;
}
}
.logout-button {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.4);
color: $text-primary;
padding: $spacing-sm $spacing-lg;
border-radius: $radius-md;
cursor: pointer;
transition: all $transition-base;
font-weight: $font-weight-medium;
&:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
}
@media (max-width: 480px) {
padding: $spacing-sm $spacing-md;
font-size: $font-size-sm;
}
}
}
}
.layout-main {
flex: 1;
max-width: 1400px;
margin: 0 auto;
padding: $spacing-2xl $spacing-xl;
width: 100%;
box-sizing: border-box;
@media (max-width: 768px) {
padding: $spacing-xl $spacing-md;
}
@media (max-width: 480px) {
padding: $spacing-lg $spacing-sm;
}
}

View File

@ -22,25 +22,39 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is already logged in on mount
const checkAuth = async () => {
try {
const token = localStorage.getItem('auth_token');
if (token) {
const currentUser = await apiService.getCurrentUser();
setUser(currentUser);
}
} catch (error) {
console.error('Auth check failed:', error);
localStorage.removeItem('auth_token');
} finally {
setLoading(false);
}
};
const isMockMode = import.meta.env.VITE_MOCK_AUTH === 'true';
useEffect(() => {
// Check if user is already logged in on mount
const checkAuth = async () => {
try {
// In mock mode, automatically set a mock user
if (isMockMode) {
setUser({
id: 'mock-user-1',
username: 'developer',
email: 'dev@coopcloud.tech',
});
setLoading(false);
return;
}
const token = localStorage.getItem('auth_token');
if (token) {
const currentUser = await apiService.getCurrentUser();
setUser(currentUser);
}
} catch (error) {
console.error('Auth check failed:', error);
localStorage.removeItem('auth_token');
} finally {
setLoading(false);
}
};
checkAuth();
}, [isMockMode]);
checkAuth();
}, []);
const login = async (credentials: LoginCredentials) => {
try {

View File

@ -1,11 +1,15 @@
@use './assets/scss/variables' as *;
@use './assets/scss/mixins' as *;
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-family: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI',
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
color: $text-primary;
background-color: $primary;
font-synthesis: none;
text-rendering: optimizeLegibility;
@ -24,8 +28,6 @@ a:hover {
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
@ -33,6 +35,8 @@ body {
h1 {
font-size: 3.2em;
line-height: 1.1;
font-family: 'Lora', serif;
color: $text-primary;
}
button {
@ -42,7 +46,7 @@ button {
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
background-color: $primary-dark;
cursor: pointer;
transition: border-color 0.25s;
}
@ -57,12 +61,12 @@ button:focus-visible {
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
background-color: $primary;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
background-color: $primary;
}
}

91
src/services/mockApi.ts Normal file
View File

@ -0,0 +1,91 @@
import type { AbraApp, AbraServer } from '../types';
// Mock data for development
export const mockApps: AbraApp[] = [
{
id: '1',
name: 'nextcloud',
domain: 'cloud.example.coop',
status: 'running',
recipe: 'nextcloud',
},
{
id: '2',
name: 'wordpress',
domain: 'blog.example.coop',
status: 'running',
recipe: 'wordpress',
},
{
id: '3',
name: 'gitea',
domain: 'git.example.coop',
status: 'stopped',
recipe: 'gitea',
},
{
id: '4',
name: 'discourse',
domain: 'forum.example.coop',
status: 'running',
recipe: 'discourse',
},
{
id: '5',
name: 'peertube',
domain: 'video.example.coop',
status: 'error',
recipe: 'peertube',
},
];
export const mockServers: AbraServer[] = [
{
name: 'prod-server-1',
host: '192.168.1.10',
user: 'root',
connected: true,
},
{
name: 'prod-server-2',
host: '192.168.1.11',
user: 'root',
connected: true,
},
{
name: 'staging-server',
host: '192.168.1.20',
user: 'root',
connected: false,
},
];
// Simulate API delay
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export const mockApiService = {
async getApps(): Promise<AbraApp[]> {
await delay(300);
return mockApps;
},
async getServers(): Promise<AbraServer[]> {
await delay(300);
return mockServers;
},
async deployApp(appName: string): Promise<void> {
await delay(1000);
console.log(`Mock: Deploying app ${appName}`);
},
async stopApp(appName: string): Promise<void> {
await delay(500);
console.log(`Mock: Stopping app ${appName}`);
},
async startApp(appName: string): Promise<void> {
await delay(500);
console.log(`Mock: Starting app ${appName}`);
},
};