source dir
This commit is contained in:
176
src/App.css
Normal file
176
src/App.css
Normal 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
182
src/App.jsx
Normal 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
27
src/api.js
Normal 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
68
src/index.css
Normal 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
10
src/main.jsx
Normal 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>,
|
||||
)
|
||||
Reference in New Issue
Block a user