feat: Add Docker Compose setup for fullstack deployment
- Add Frontend Dockerfile with Next.js standalone build - Add docker-compose.yml for production deployment - Add docker-compose.dev.yml for development with hot-reload - Configure Frontend next.config.js with standalone output - Add .dockerignore files for both backend and frontend - Add comprehensive README-DOCKER.md documentation - Update .gitignore to exclude node_modules and build artifacts - Remove obsolete component files (CycleCalculator.tsx, PHDiagram.tsx) - Backend and Frontend communicate via Docker network - Healthchecks configured for both services - Environment variables configured for API URL
This commit is contained in:
30
Frontend/src/app/globals.css
Normal file
30
Frontend/src/app/globals.css
Normal file
@@ -0,0 +1,30 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: #0a0e1a;
|
||||
min-height: 100vh;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0a0e1a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #2d3548;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #3d4558;
|
||||
}
|
||||
19
Frontend/src/app/layout.tsx
Normal file
19
Frontend/src/app/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Diagram PH - Refrigeration Cycle Calculator',
|
||||
description: 'Calculate and visualize refrigeration cycles',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
67
Frontend/src/app/page.tsx
Normal file
67
Frontend/src/app/page.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import PHDiagramModern from "@/components/PHDiagramModern";
|
||||
import CycleCalculatorModern from "@/components/CycleCalculatorModern";
|
||||
|
||||
export default function Home() {
|
||||
const [activeView, setActiveView] = useState<'calculator' | 'diagram'>('diagram');
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: '2rem',
|
||||
right: '2rem',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
gap: '0.75rem',
|
||||
background: 'rgba(30, 58, 95, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setActiveView('diagram')}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
background: activeView === 'diagram' ? 'linear-gradient(135deg, #38b2ac 0%, #2c7a7b 100%)' : 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: '12px',
|
||||
color: '#ffffff',
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: '700',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s',
|
||||
boxShadow: activeView === 'diagram' ? '0 4px 12px rgba(56, 178, 172, 0.4)' : 'none',
|
||||
letterSpacing: '0.3px'
|
||||
}}
|
||||
>
|
||||
📊 P-h Diagram
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('calculator')}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
background: activeView === 'calculator' ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: '12px',
|
||||
color: '#ffffff',
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: '700',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s',
|
||||
boxShadow: activeView === 'calculator' ? '0 4px 12px rgba(59, 130, 246, 0.4)' : 'none',
|
||||
letterSpacing: '0.3px'
|
||||
}}
|
||||
>
|
||||
🧮 Cycle Calculator
|
||||
</button>
|
||||
</div>
|
||||
{activeView === 'diagram' && <PHDiagramModern />}
|
||||
{activeView === 'calculator' && <CycleCalculatorModern />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1067
Frontend/src/components/CycleCalculatorModern.tsx
Normal file
1067
Frontend/src/components/CycleCalculatorModern.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1498
Frontend/src/components/PHDiagramModern.tsx
Normal file
1498
Frontend/src/components/PHDiagramModern.tsx
Normal file
File diff suppressed because it is too large
Load Diff
65
Frontend/src/lib/api-client.ts
Normal file
65
Frontend/src/lib/api-client.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
CycleCalculationRequest,
|
||||
CycleCalculationResponse,
|
||||
DiagramRequest,
|
||||
DiagramResponse,
|
||||
RefrigerantInfo,
|
||||
RefrigerantsListResponse,
|
||||
} from '@/types/api';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8001/api/v1';
|
||||
|
||||
export class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string = API_BASE_URL) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
async getRefrigerants(): Promise<RefrigerantInfo[]> {
|
||||
const response = await fetch(`${this.baseUrl}/refrigerants/`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch refrigerants');
|
||||
}
|
||||
const data: RefrigerantsListResponse = await response.json();
|
||||
return data.refrigerants;
|
||||
}
|
||||
|
||||
async calculateCycle(
|
||||
request: CycleCalculationRequest
|
||||
): Promise<CycleCalculationResponse> {
|
||||
const response = await fetch(`${this.baseUrl}/cycles/simple`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to calculate cycle');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async generateDiagram(request: DiagramRequest): Promise<DiagramResponse> {
|
||||
const response = await fetch(`${this.baseUrl}/diagrams/ph`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to generate diagram');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
84
Frontend/src/types/api.ts
Normal file
84
Frontend/src/types/api.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
export interface CycleCalculationRequest {
|
||||
refrigerant: string;
|
||||
evap_temperature?: number;
|
||||
cond_temperature?: number;
|
||||
evap_pressure?: number;
|
||||
cond_pressure?: number;
|
||||
compressor_efficiency?: number;
|
||||
superheat?: number;
|
||||
subcool?: number;
|
||||
mass_flow?: number;
|
||||
}
|
||||
|
||||
export interface CyclePoint {
|
||||
point_id: string;
|
||||
pressure: number;
|
||||
temperature?: number;
|
||||
enthalpy?: number;
|
||||
entropy?: number;
|
||||
quality?: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CyclePerformance {
|
||||
cop: number;
|
||||
cooling_capacity: number;
|
||||
heating_capacity: number;
|
||||
compressor_power: number;
|
||||
compressor_efficiency: number;
|
||||
mass_flow: number;
|
||||
volumetric_flow?: number;
|
||||
compression_ratio: number;
|
||||
discharge_temperature: number;
|
||||
}
|
||||
|
||||
export interface CycleCalculationResponse {
|
||||
success: boolean;
|
||||
refrigerant: string;
|
||||
points: CyclePoint[];
|
||||
performance: CyclePerformance;
|
||||
diagram_data?: any;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface DiagramRequest {
|
||||
refrigerant: string;
|
||||
pressure_range: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
enthalpy_range?: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
include_isotherms?: boolean;
|
||||
isotherm_values?: number[];
|
||||
cycle_points?: Array<{ enthalpy: number; pressure: number }>;
|
||||
title?: string;
|
||||
format?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
dpi?: number;
|
||||
}
|
||||
|
||||
export interface DiagramResponse {
|
||||
success: boolean;
|
||||
image?: string;
|
||||
data?: any;
|
||||
metadata: any;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface RefrigerantInfo {
|
||||
name: string;
|
||||
formula?: string;
|
||||
available: boolean;
|
||||
loaded?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface RefrigerantsListResponse {
|
||||
refrigerants: RefrigerantInfo[];
|
||||
total: number;
|
||||
available_count: number;
|
||||
}
|
||||
Reference in New Issue
Block a user