dashboard backbone and scss work
This commit is contained in:
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_API_URL=http://localhost:3000/api
|
||||
VITE_MOCK_AUTH=true
|
||||
5
.env.example
Normal file
5
.env.example
Normal 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
|
||||
@ -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>
|
||||
|
||||
140
public/coopcloud_logo_grey.svg
Normal file
140
public/coopcloud_logo_grey.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 163 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
31
src/assets/scss/_fonts.scss
Normal file
31
src/assets/scss/_fonts.scss
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// 1. Configuration & helpers (no CSS output)
|
||||
@use 'variables';
|
||||
@use 'mixins';
|
||||
@use 'fonts';
|
||||
|
||||
// 2. Global base styles
|
||||
@use 'global';
|
||||
@use 'global';
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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">
|
||||
|
||||
156
src/components/layout/_Layout.scss
Normal file
156
src/components/layout/_Layout.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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
91
src/services/mockApi.ts
Normal 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}`);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user