create-react-my-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/index.js +77 -0
- package/package.json +32 -0
- package/template/backend/config/db.js +21 -0
- package/template/backend/controllers/authcontroller.js +81 -0
- package/template/backend/controllers/reportcontroller.js +31 -0
- package/template/backend/database/schema.sql +21 -0
- package/template/backend/middleware/authentication.js +10 -0
- package/template/backend/package-lock.json +1417 -0
- package/template/backend/package.json +19 -0
- package/template/backend/routes/authroute.js +12 -0
- package/template/backend/routes/reportroute.js +7 -0
- package/template/backend/server.js +55 -0
- package/template/frontend/eslint.config.js +29 -0
- package/template/frontend/index.html +12 -0
- package/template/frontend/package-lock.json +3934 -0
- package/template/frontend/package.json +31 -0
- package/template/frontend/public/vite.svg +1 -0
- package/template/frontend/src/App.css +0 -0
- package/template/frontend/src/App.jsx +38 -0
- package/template/frontend/src/assets/react.svg +1 -0
- package/template/frontend/src/components/Button.jsx +44 -0
- package/template/frontend/src/components/Form.jsx +150 -0
- package/template/frontend/src/components/Navbar.jsx +83 -0
- package/template/frontend/src/components/Table.jsx +77 -0
- package/template/frontend/src/components/protectedroute.jsx +41 -0
- package/template/frontend/src/config/api.js +8 -0
- package/template/frontend/src/index.css +46 -0
- package/template/frontend/src/main.jsx +10 -0
- package/template/frontend/src/pages/login.jsx +42 -0
- package/template/frontend/src/pages/report.jsx +90 -0
- package/template/frontend/src/pages/signup.jsx +46 -0
- package/template/frontend/src/pages/trial.jsx +87 -0
- package/template/frontend/vite.config.js +10 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import Navbar from '../components/Navbar';
|
|
3
|
+
import Button from '../components/Button';
|
|
4
|
+
import Table from '../components/Table';
|
|
5
|
+
import { api } from '../config/api';
|
|
6
|
+
|
|
7
|
+
const columns = [
|
|
8
|
+
{ key: 'id', label: 'ID' },
|
|
9
|
+
{ key: 'title', label: 'Title' },
|
|
10
|
+
{ key: 'category', label: 'Category' },
|
|
11
|
+
{ key: 'amount', label: 'Amount', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
12
|
+
{ key: 'report_date', label: 'Date' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const Report = () => {
|
|
16
|
+
const [reportData, setReportData] = useState([]);
|
|
17
|
+
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [error, setError] = useState('');
|
|
20
|
+
const [message, setMessage] = useState('');
|
|
21
|
+
|
|
22
|
+
const fetchReport = async () => {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
setError('');
|
|
25
|
+
setMessage('');
|
|
26
|
+
try {
|
|
27
|
+
const res = await api.get(`/report?date=${date}`);
|
|
28
|
+
setReportData(res.data.data ?? []);
|
|
29
|
+
setMessage(res.data.message);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
setError(err.response?.data?.message || 'Could not load report');
|
|
32
|
+
setReportData([]);
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const exportCsv = () => {
|
|
39
|
+
if (!reportData.length) return;
|
|
40
|
+
const headers = columns.map((c) => c.label).join(',');
|
|
41
|
+
const rows = reportData.map((row) => columns.map((c) => row[c.key]).join(','));
|
|
42
|
+
const blob = new Blob([[headers, ...rows].join('\n')], { type: 'text/csv' });
|
|
43
|
+
const url = URL.createObjectURL(blob);
|
|
44
|
+
const a = document.createElement('a');
|
|
45
|
+
a.href = url;
|
|
46
|
+
a.download = `report_${date}.csv`;
|
|
47
|
+
a.click();
|
|
48
|
+
URL.revokeObjectURL(url);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<Navbar />
|
|
54
|
+
<div className="page-shell max-w-6xl mx-auto px-5 py-10 sm:py-14">
|
|
55
|
+
<header className="mb-10 animate-fade-in">
|
|
56
|
+
<h1 className="text-2xl sm:text-3xl font-semibold text-white">Report</h1>
|
|
57
|
+
<p className="text-neutral-500 text-sm mt-2">Generate a report for any date.</p>
|
|
58
|
+
</header>
|
|
59
|
+
|
|
60
|
+
<div className="border border-neutral-800 rounded-sm p-6 mb-8 animate-fade-in no-print">
|
|
61
|
+
<div className="flex flex-col sm:flex-row gap-4">
|
|
62
|
+
<input
|
|
63
|
+
type="date"
|
|
64
|
+
value={date}
|
|
65
|
+
onChange={(e) => setDate(e.target.value)}
|
|
66
|
+
className="flex-1 h-11 px-4 bg-black border border-neutral-700 rounded-sm text-white focus:outline-none focus:border-white transition-colors"
|
|
67
|
+
/>
|
|
68
|
+
<Button size="md" theme="white" onClick={fetchReport} loading={loading} className="sm:min-w-[160px]">
|
|
69
|
+
Generate
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
{error && <p className="text-sm text-neutral-400 mt-4 text-center">{error}</p>}
|
|
73
|
+
{message && !error && <p className="text-sm text-neutral-500 mt-4 text-center">{message}</p>}
|
|
74
|
+
{reportData.length > 0 && (
|
|
75
|
+
<div className="flex flex-wrap gap-3 mt-6 pt-6 border-t border-neutral-800">
|
|
76
|
+
<Button size="sm" theme="white" onClick={exportCsv}>Export CSV</Button>
|
|
77
|
+
<Button size="sm" theme="black" onClick={() => window.print()}>Print</Button>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{(reportData.length > 0 || (message && !error)) && (
|
|
83
|
+
<Table title={`Results — ${date}`} columns={columns} data={reportData} emptyMessage="No entries for this date" />
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default Report;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
3
|
+
import Form from '../components/Form';
|
|
4
|
+
import { api } from '../config/api';
|
|
5
|
+
|
|
6
|
+
const fields = [
|
|
7
|
+
{ name: 'username', label: 'Username', placeholder: 'Choose username', required: true, minLength: 2 },
|
|
8
|
+
{ name: 'password', label: 'Password', type: 'password', placeholder: 'Create password', required: true },
|
|
9
|
+
{
|
|
10
|
+
name: 'confirmPassword', label: 'Confirm', type: 'password', placeholder: 'Repeat password', required: true, validatePassword: false,
|
|
11
|
+
validate: (v, all) => (all?.password && v !== all.password ? 'Passwords do not match' : ''),
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const Signup = () => {
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
|
|
20
|
+
const handleSubmit = async (data) => {
|
|
21
|
+
try {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
setError('');
|
|
24
|
+
await api.post('/auth/register', { username: data.username, password: data.password });
|
|
25
|
+
navigate('/trial');
|
|
26
|
+
} catch (err) {
|
|
27
|
+
setError(err.response?.data?.message || 'Signup failed');
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="min-h-screen flex items-center justify-center px-5 py-16">
|
|
35
|
+
<div className="w-full max-w-md animate-fade-in">
|
|
36
|
+
<p className="text-center text-xs uppercase tracking-[0.3em] text-neutral-500 mb-8">Get started</p>
|
|
37
|
+
<Form title="Sign up" fields={fields} onSubmit={handleSubmit} loading={loading} error={error} submitText="Create account" submitTheme="white" />
|
|
38
|
+
<p className="text-center text-sm text-neutral-500 mt-8">
|
|
39
|
+
Have an account? <Link to="/login" className="text-white hover:underline">Sign in</Link>
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default Signup;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import Navbar from '../components/Navbar';
|
|
3
|
+
import Form from '../components/Form';
|
|
4
|
+
import Table from '../components/Table';
|
|
5
|
+
|
|
6
|
+
const formFields = [
|
|
7
|
+
{ name: 'name', label: 'Name', placeholder: 'Item name', required: true },
|
|
8
|
+
{
|
|
9
|
+
name: 'category',
|
|
10
|
+
label: 'Category',
|
|
11
|
+
type: 'select',
|
|
12
|
+
placeholder: 'Pick category',
|
|
13
|
+
required: true,
|
|
14
|
+
options: [
|
|
15
|
+
{ value: 'electronics', label: 'Electronics' },
|
|
16
|
+
{ value: 'accessories', label: 'Accessories' },
|
|
17
|
+
{ value: 'office', label: 'Office' },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{ name: 'quantity', label: 'Quantity', type: 'number', placeholder: '0', required: true, min: 1 },
|
|
21
|
+
{ name: 'price', label: 'Price', type: 'number', placeholder: '0.00', required: true, min: 0 },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const columns = [
|
|
25
|
+
{ key: 'name', label: 'Name' },
|
|
26
|
+
{ key: 'category', label: 'Category' },
|
|
27
|
+
{ key: 'quantity', label: 'Qty' },
|
|
28
|
+
{ key: 'price', label: 'Price', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const Trial = () => {
|
|
32
|
+
const [items, setItems] = useState([
|
|
33
|
+
{ id: 1, name: 'Keyboard', category: 'electronics', quantity: 12, price: 49.99 },
|
|
34
|
+
{ id: 2, name: 'Notebook', category: 'office', quantity: 40, price: 3.5 },
|
|
35
|
+
]);
|
|
36
|
+
const [editingId, setEditingId] = useState(null);
|
|
37
|
+
|
|
38
|
+
const handleSubmit = (data) => {
|
|
39
|
+
if (editingId) {
|
|
40
|
+
setItems((prev) => prev.map((item) => (item.id === editingId ? { ...item, ...data, id: editingId } : item)));
|
|
41
|
+
setEditingId(null);
|
|
42
|
+
} else {
|
|
43
|
+
setItems((prev) => [...prev, { id: Date.now(), ...data }]);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const editingItem = items.find((item) => item.id === editingId);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<Navbar />
|
|
52
|
+
<div className="page-shell max-w-full mx-auto px-5 py-10 sm:py-14">
|
|
53
|
+
<header className="mb-10 animate-fade-in">
|
|
54
|
+
<h1 className="text-2xl sm:text-3xl font-semibold text-white tracking-tight">Form & Table</h1>
|
|
55
|
+
<p className="text-neutral-500 mt-2 text-sm max-w-lg">
|
|
56
|
+
Add rows with the form, then edit or remove them from the table.
|
|
57
|
+
</p>
|
|
58
|
+
</header>
|
|
59
|
+
|
|
60
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-10 items-start">
|
|
61
|
+
<Form
|
|
62
|
+
key={editingId ?? 'new'}
|
|
63
|
+
title={editingId ? 'Edit item' : 'Add item'}
|
|
64
|
+
fields={formFields}
|
|
65
|
+
onSubmit={handleSubmit}
|
|
66
|
+
submitText={editingId ? 'Save changes' : 'Add to table'}
|
|
67
|
+
submitTheme="white"
|
|
68
|
+
initialValues={editingItem ?? {}}
|
|
69
|
+
/>
|
|
70
|
+
<Table
|
|
71
|
+
title="Items"
|
|
72
|
+
columns={columns}
|
|
73
|
+
data={items}
|
|
74
|
+
onEdit={(row) => setEditingId(row.id)}
|
|
75
|
+
onDelete={(row) => {
|
|
76
|
+
setItems((prev) => prev.filter((i) => i.id !== row.id));
|
|
77
|
+
if (editingId === row.id) setEditingId(null);
|
|
78
|
+
}}
|
|
79
|
+
emptyMessage="Nothing here yet — add something above"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default Trial;
|