source dir

This commit is contained in:
2026-02-06 17:16:27 +00:00
parent b772727f4a
commit 49d4c5e69b
5 changed files with 463 additions and 0 deletions

176
src/App.css Normal file
View File

@@ -0,0 +1,176 @@
/* Simple styling for inventory app - no framework, just basic CSS */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 3rem;
border-bottom: 2px solid #646cff;
padding-bottom: 1rem;
}
header h1 {
margin: 0;
color: #646cff;
}
header p {
margin: 0.5rem 0 0 0;
color: #888;
}
/* Form Section */
.form-section {
background: #1a1a1a;
padding: 2rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.form-section h2 {
margin-top: 0;
color: #646cff;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 500px;
}
.form-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.form-group label {
font-weight: bold;
color: #fff;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #444;
border-radius: 4px;
background: #2a2a2a;
color: #fff;
font-size: 1rem;
}
.form-group input:focus {
outline: none;
border-color: #646cff;
}
.form-group input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button[type="submit"] {
padding: 0.75rem 2rem;
background: #646cff;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: background 0.3s;
}
button[type="submit"]:hover:not(:disabled) {
background: #535bf2;
}
button[type="submit"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* List Section */
.list-section {
background: #1a1a1a;
padding: 2rem;
border-radius: 8px;
}
.list-section h2 {
margin-top: 0;
color: #646cff;
}
.loading, .error, .empty {
text-align: center;
padding: 2rem;
font-size: 1.1rem;
}
.loading {
color: #646cff;
}
.error {
color: #ff6b6b;
}
.empty {
color: #888;
}
/* Table Styles */
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
thead {
background: #2a2a2a;
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #444;
}
th {
font-weight: bold;
color: #646cff;
}
td {
color: #fff;
}
tbody tr:hover {
background: #2a2a2a;
}
tbody tr:last-child td {
border-bottom: none;
}
/* Responsive */
@media (max-width: 768px) {
.container {
padding: 1rem;
}
table {
font-size: 0.9rem;
}
th, td {
padding: 0.5rem;
}
}

182
src/App.jsx Normal file
View File

@@ -0,0 +1,182 @@
// Single component app - list + form (minimal design)
import { useState, useEffect } from 'react';
import { getItems, createItem } from './api';
import './App.css';
function App() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [formData, setFormData] = useState({
name: '',
quantity: '',
price: ''
});
const [submitting, setSubmitting] = useState(false);
// Fetch items on mount
useEffect(() => {
fetchItems();
}, []);
const fetchItems = async () => {
try {
setLoading(true);
setError(null);
const data = await getItems();
setItems(data);
} catch (err) {
setError('Failed to load items: ' + err.message);
console.error('Error fetching items:', err);
} finally {
setLoading(false);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
// Basic validation
if (!formData.name.trim()) {
alert('Name is required');
return;
}
if (formData.quantity === '' || formData.quantity < 0) {
alert('Valid quantity is required');
return;
}
if (formData.price === '' || formData.price < 0) {
alert('Valid price is required');
return;
}
try {
setSubmitting(true);
await createItem({
name: formData.name.trim(),
quantity: parseInt(formData.quantity),
price: parseFloat(formData.price)
});
// Reset form
setFormData({ name: '', quantity: '', price: '' });
// Refresh list
await fetchItems();
alert('Item added successfully!');
} catch (err) {
alert('Failed to add item: ' + err.message);
console.error('Error creating item:', err);
} finally {
setSubmitting(false);
}
};
return (
<div className="container">
<header>
<h1>Inventory Management</h1>
<p>Simple 2-tier app for DevOps demo</p>
</header>
{/* Add Item Form */}
<section className="form-section">
<h2>Add New Item</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Enter item name"
disabled={submitting}
required
/>
</div>
<div className="form-group">
<label htmlFor="quantity">Quantity:</label>
<input
type="number"
id="quantity"
name="quantity"
value={formData.quantity}
onChange={handleInputChange}
placeholder="Enter quantity"
min="0"
disabled={submitting}
required
/>
</div>
<div className="form-group">
<label htmlFor="price">Price:</label>
<input
type="number"
id="price"
name="price"
value={formData.price}
onChange={handleInputChange}
placeholder="Enter price"
min="0"
step="0.01"
disabled={submitting}
required
/>
</div>
<button type="submit" disabled={submitting}>
{submitting ? 'Adding...' : 'Add Item'}
</button>
</form>
</section>
{/* Items List */}
<section className="list-section">
<h2>Items List</h2>
{loading && <p className="loading">Loading items...</p>}
{error && <p className="error">{error}</p>}
{!loading && !error && items.length === 0 && (
<p className="empty">No items found. Add one above!</p>
)}
{!loading && !error && items.length > 0 && (
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Quantity</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{items.map(item => (
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.name}</td>
<td>{item.quantity}</td>
<td>${parseFloat(item.price).toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
)}
</section>
</div>
);
}
export default App;

27
src/api.js Normal file
View File

@@ -0,0 +1,27 @@
// API service for inventory management
import axios from 'axios';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
// Create axios instance with base configuration
const api = axios.create({
baseURL: API_URL,
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});
// Get all items
export const getItems = async () => {
const response = await api.get('/api/items');
return response.data;
};
// Create new item
export const createItem = async (itemData) => {
const response = await api.post('/api/items', itemData);
return response.data;
};
export default api;

68
src/index.css Normal file
View File

@@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

10
src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)