create-steve-rogers 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/apps/HMSR/backend/.env.example +6 -0
- package/apps/HMSR/backend/config/db.js +14 -0
- package/apps/HMSR/backend/database/schema.sql +55 -0
- package/apps/HMSR/backend/database/seed.js +53 -0
- package/apps/HMSR/backend/middleware/auth.js +26 -0
- package/apps/HMSR/backend/package-lock.json +1130 -0
- package/apps/HMSR/backend/package.json +19 -0
- package/apps/HMSR/backend/routes/appointments.js +157 -0
- package/apps/HMSR/backend/routes/auth.js +115 -0
- package/apps/HMSR/backend/routes/medicalReports.js +126 -0
- package/apps/HMSR/backend/routes/patients.js +103 -0
- package/apps/HMSR/backend/routes/reports.js +60 -0
- package/apps/HMSR/backend/server.js +38 -0
- package/apps/HMSR/frontend/package-lock.json +17217 -0
- package/apps/HMSR/frontend/package.json +23 -0
- package/apps/HMSR/frontend/public/index.html +17 -0
- package/apps/HMSR/frontend/src/App.js +91 -0
- package/apps/HMSR/frontend/src/components/Layout.js +58 -0
- package/apps/HMSR/frontend/src/components/PrivateRoute.js +25 -0
- package/apps/HMSR/frontend/src/context/AuthContext.js +54 -0
- package/apps/HMSR/frontend/src/index.css +581 -0
- package/apps/HMSR/frontend/src/index.js +17 -0
- package/apps/HMSR/frontend/src/pages/Appointments.js +250 -0
- package/apps/HMSR/frontend/src/pages/Dashboard.js +116 -0
- package/apps/HMSR/frontend/src/pages/Login.js +73 -0
- package/apps/HMSR/frontend/src/pages/MedicalReports.js +217 -0
- package/apps/HMSR/frontend/src/pages/Patients.js +196 -0
- package/apps/HMSR/frontend/src/pages/Register.js +98 -0
- package/apps/HMSR/frontend/src/pages/Reports.js +170 -0
- package/apps/HMSR/frontend/src/services/api.js +15 -0
- package/apps/config.js +8 -0
- package/exclude.txt +1 -0
- package/index.js +55 -0
- package/package.json +14 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import api from '../services/api';
|
|
3
|
+
|
|
4
|
+
const emptyForm = {
|
|
5
|
+
full_name: '',
|
|
6
|
+
gender: 'Male',
|
|
7
|
+
date_of_birth: '',
|
|
8
|
+
phone_number: '',
|
|
9
|
+
address: '',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function Patients() {
|
|
13
|
+
const [patients, setPatients] = useState([]);
|
|
14
|
+
const [form, setForm] = useState(emptyForm);
|
|
15
|
+
const [editingId, setEditingId] = useState(null);
|
|
16
|
+
const [showForm, setShowForm] = useState(false);
|
|
17
|
+
const [error, setError] = useState('');
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
|
|
20
|
+
const fetchPatients = async () => {
|
|
21
|
+
try {
|
|
22
|
+
const res = await api.get('/patients');
|
|
23
|
+
setPatients(res.data);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
setError(err.response?.data?.message || 'Failed to load patients.');
|
|
26
|
+
} finally {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
fetchPatients();
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const handleChange = (e) => {
|
|
36
|
+
setForm({ ...form, [e.target.name]: e.target.value });
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleSubmit = async (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
setError('');
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (editingId) {
|
|
45
|
+
await api.put(`/patients/${editingId}`, form);
|
|
46
|
+
} else {
|
|
47
|
+
await api.post('/patients', form);
|
|
48
|
+
}
|
|
49
|
+
setForm(emptyForm);
|
|
50
|
+
setEditingId(null);
|
|
51
|
+
setShowForm(false);
|
|
52
|
+
fetchPatients();
|
|
53
|
+
} catch (err) {
|
|
54
|
+
setError(err.response?.data?.message || 'Operation failed.');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleEdit = (patient) => {
|
|
59
|
+
setForm({
|
|
60
|
+
full_name: patient.full_name,
|
|
61
|
+
gender: patient.gender,
|
|
62
|
+
date_of_birth: patient.date_of_birth?.split('T')[0] || patient.date_of_birth,
|
|
63
|
+
phone_number: patient.phone_number,
|
|
64
|
+
address: patient.address,
|
|
65
|
+
});
|
|
66
|
+
setEditingId(patient.id);
|
|
67
|
+
setShowForm(true);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleDelete = async (id) => {
|
|
71
|
+
if (!window.confirm('Delete this patient record?')) return;
|
|
72
|
+
try {
|
|
73
|
+
await api.delete(`/patients/${id}`);
|
|
74
|
+
fetchPatients();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
setError(err.response?.data?.message || 'Delete failed.');
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const cancelForm = () => {
|
|
81
|
+
setForm(emptyForm);
|
|
82
|
+
setEditingId(null);
|
|
83
|
+
setShowForm(false);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="page">
|
|
88
|
+
<header className="page-header">
|
|
89
|
+
<div>
|
|
90
|
+
<h1>Patient Management</h1>
|
|
91
|
+
<p>Register and manage patient records</p>
|
|
92
|
+
</div>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
className="btn btn-primary"
|
|
96
|
+
onClick={() => {
|
|
97
|
+
setShowForm(!showForm);
|
|
98
|
+
if (showForm) cancelForm();
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{showForm ? 'Close Form' : '+ Add Patient'}
|
|
102
|
+
</button>
|
|
103
|
+
</header>
|
|
104
|
+
|
|
105
|
+
{error && <div className="alert alert-error">{error}</div>}
|
|
106
|
+
|
|
107
|
+
{showForm && (
|
|
108
|
+
<div className="card form-card">
|
|
109
|
+
<h2>{editingId ? 'Update Patient' : 'New Patient'}</h2>
|
|
110
|
+
<form onSubmit={handleSubmit} className="form-grid">
|
|
111
|
+
<div className="form-group">
|
|
112
|
+
<label>Full Name</label>
|
|
113
|
+
<input name="full_name" value={form.full_name} onChange={handleChange} required />
|
|
114
|
+
</div>
|
|
115
|
+
<div className="form-group">
|
|
116
|
+
<label>Gender</label>
|
|
117
|
+
<select name="gender" value={form.gender} onChange={handleChange}>
|
|
118
|
+
<option value="Male">Male</option>
|
|
119
|
+
<option value="Female">Female</option>
|
|
120
|
+
<option value="Other">Other</option>
|
|
121
|
+
</select>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="form-group">
|
|
124
|
+
<label>Date of Birth</label>
|
|
125
|
+
<input
|
|
126
|
+
name="date_of_birth"
|
|
127
|
+
type="date"
|
|
128
|
+
value={form.date_of_birth}
|
|
129
|
+
onChange={handleChange}
|
|
130
|
+
required
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="form-group">
|
|
134
|
+
<label>Phone Number</label>
|
|
135
|
+
<input name="phone_number" value={form.phone_number} onChange={handleChange} required />
|
|
136
|
+
</div>
|
|
137
|
+
<div className="form-group full-width">
|
|
138
|
+
<label>Address</label>
|
|
139
|
+
<textarea name="address" value={form.address} onChange={handleChange} rows={2} required />
|
|
140
|
+
</div>
|
|
141
|
+
<div className="form-actions full-width">
|
|
142
|
+
<button type="submit" className="btn btn-primary">
|
|
143
|
+
{editingId ? 'Update' : 'Save'} Patient
|
|
144
|
+
</button>
|
|
145
|
+
<button type="button" className="btn btn-outline" onClick={cancelForm}>
|
|
146
|
+
Cancel
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
</form>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
<div className="card">
|
|
154
|
+
{loading ? (
|
|
155
|
+
<p className="text-muted">Loading patients...</p>
|
|
156
|
+
) : patients.length === 0 ? (
|
|
157
|
+
<p className="text-muted">No patients registered yet.</p>
|
|
158
|
+
) : (
|
|
159
|
+
<div className="table-responsive">
|
|
160
|
+
<table>
|
|
161
|
+
<thead>
|
|
162
|
+
<tr>
|
|
163
|
+
<th>Name</th>
|
|
164
|
+
<th>Gender</th>
|
|
165
|
+
<th>Date of Birth</th>
|
|
166
|
+
<th>Phone</th>
|
|
167
|
+
<th>Address</th>
|
|
168
|
+
<th>Actions</th>
|
|
169
|
+
</tr>
|
|
170
|
+
</thead>
|
|
171
|
+
<tbody>
|
|
172
|
+
{patients.map((p) => (
|
|
173
|
+
<tr key={p.id}>
|
|
174
|
+
<td>{p.full_name}</td>
|
|
175
|
+
<td>{p.gender}</td>
|
|
176
|
+
<td>{p.date_of_birth?.split('T')[0]}</td>
|
|
177
|
+
<td>{p.phone_number}</td>
|
|
178
|
+
<td>{p.address}</td>
|
|
179
|
+
<td className="actions">
|
|
180
|
+
<button type="button" className="btn btn-sm btn-outline" onClick={() => handleEdit(p)}>
|
|
181
|
+
Edit
|
|
182
|
+
</button>
|
|
183
|
+
<button type="button" className="btn btn-sm btn-danger" onClick={() => handleDelete(p.id)}>
|
|
184
|
+
Delete
|
|
185
|
+
</button>
|
|
186
|
+
</td>
|
|
187
|
+
</tr>
|
|
188
|
+
))}
|
|
189
|
+
</tbody>
|
|
190
|
+
</table>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../context/AuthContext';
|
|
4
|
+
|
|
5
|
+
export default function Register() {
|
|
6
|
+
const [form, setForm] = useState({
|
|
7
|
+
full_name: '',
|
|
8
|
+
email: '',
|
|
9
|
+
password: '',
|
|
10
|
+
role: 'receptionist',
|
|
11
|
+
});
|
|
12
|
+
const [error, setError] = useState('');
|
|
13
|
+
const [success, setSuccess] = useState('');
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
const { register } = useAuth();
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
|
|
18
|
+
const handleChange = (e) => {
|
|
19
|
+
setForm({ ...form, [e.target.name]: e.target.value });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const handleSubmit = async (e) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setError('');
|
|
25
|
+
setSuccess('');
|
|
26
|
+
setLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
await register(form);
|
|
29
|
+
setSuccess('Registration successful! You can now login.');
|
|
30
|
+
setTimeout(() => navigate('/login'), 2000);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
setError(err.response?.data?.message || 'Registration failed.');
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="auth-page">
|
|
40
|
+
<div className="auth-card">
|
|
41
|
+
<div className="auth-header">
|
|
42
|
+
<h1>Create Account</h1>
|
|
43
|
+
<p>Register as Receptionist or Doctor</p>
|
|
44
|
+
</div>
|
|
45
|
+
<form onSubmit={handleSubmit}>
|
|
46
|
+
{error && <div className="alert alert-error">{error}</div>}
|
|
47
|
+
{success && <div className="alert alert-success">{success}</div>}
|
|
48
|
+
<div className="form-group">
|
|
49
|
+
<label htmlFor="full_name">Full Name</label>
|
|
50
|
+
<input
|
|
51
|
+
id="full_name"
|
|
52
|
+
name="full_name"
|
|
53
|
+
value={form.full_name}
|
|
54
|
+
onChange={handleChange}
|
|
55
|
+
required
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="form-group">
|
|
59
|
+
<label htmlFor="email">Email</label>
|
|
60
|
+
<input
|
|
61
|
+
id="email"
|
|
62
|
+
name="email"
|
|
63
|
+
type="email"
|
|
64
|
+
value={form.email}
|
|
65
|
+
onChange={handleChange}
|
|
66
|
+
required
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="form-group">
|
|
70
|
+
<label htmlFor="password">Password</label>
|
|
71
|
+
<input
|
|
72
|
+
id="password"
|
|
73
|
+
name="password"
|
|
74
|
+
type="password"
|
|
75
|
+
value={form.password}
|
|
76
|
+
onChange={handleChange}
|
|
77
|
+
minLength={6}
|
|
78
|
+
required
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="form-group">
|
|
82
|
+
<label htmlFor="role">Role</label>
|
|
83
|
+
<select id="role" name="role" value={form.role} onChange={handleChange}>
|
|
84
|
+
<option value="receptionist">Receptionist</option>
|
|
85
|
+
<option value="doctor">Doctor</option>
|
|
86
|
+
</select>
|
|
87
|
+
</div>
|
|
88
|
+
<button type="submit" className="btn btn-primary btn-block" disabled={loading}>
|
|
89
|
+
{loading ? 'Registering...' : 'Register'}
|
|
90
|
+
</button>
|
|
91
|
+
</form>
|
|
92
|
+
<p className="auth-footer">
|
|
93
|
+
Already have an account? <Link to="/login">Login here</Link>
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import api from '../services/api';
|
|
3
|
+
|
|
4
|
+
export default function Reports() {
|
|
5
|
+
const [startDate, setStartDate] = useState('');
|
|
6
|
+
const [endDate, setEndDate] = useState('');
|
|
7
|
+
const [report, setReport] = useState(null);
|
|
8
|
+
const [error, setError] = useState('');
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
|
|
11
|
+
const handleGenerate = async (e) => {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
setError('');
|
|
14
|
+
setReport(null);
|
|
15
|
+
|
|
16
|
+
if (!startDate || !endDate) {
|
|
17
|
+
setError('Please select both start and end dates.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (startDate > endDate) {
|
|
22
|
+
setError('Start date must be before or equal to end date.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
const res = await api.get('/reports/summary', {
|
|
29
|
+
params: { start_date: startDate, end_date: endDate },
|
|
30
|
+
});
|
|
31
|
+
setReport(res.data);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
setError(err.response?.data?.message || 'Failed to generate report.');
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="page">
|
|
41
|
+
<header className="page-header">
|
|
42
|
+
<div>
|
|
43
|
+
<h1>Report Generation</h1>
|
|
44
|
+
<p>Filter appointments and patients attended by date range</p>
|
|
45
|
+
</div>
|
|
46
|
+
</header>
|
|
47
|
+
|
|
48
|
+
<div className="card form-card">
|
|
49
|
+
<form onSubmit={handleGenerate} className="report-filter-form">
|
|
50
|
+
<div className="form-group">
|
|
51
|
+
<label htmlFor="start_date">Start Date</label>
|
|
52
|
+
<input
|
|
53
|
+
id="start_date"
|
|
54
|
+
type="date"
|
|
55
|
+
value={startDate}
|
|
56
|
+
onChange={(e) => setStartDate(e.target.value)}
|
|
57
|
+
required
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="form-group">
|
|
61
|
+
<label htmlFor="end_date">End Date</label>
|
|
62
|
+
<input
|
|
63
|
+
id="end_date"
|
|
64
|
+
type="date"
|
|
65
|
+
value={endDate}
|
|
66
|
+
onChange={(e) => setEndDate(e.target.value)}
|
|
67
|
+
required
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<button type="submit" className="btn btn-primary" disabled={loading}>
|
|
71
|
+
{loading ? 'Generating...' : 'Generate Report'}
|
|
72
|
+
</button>
|
|
73
|
+
</form>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{error && <div className="alert alert-error">{error}</div>}
|
|
77
|
+
|
|
78
|
+
{report && (
|
|
79
|
+
<>
|
|
80
|
+
<div className="stats-grid">
|
|
81
|
+
<div className="stat-card highlight">
|
|
82
|
+
<span className="stat-label">Total Appointments</span>
|
|
83
|
+
<span className="stat-value">{report.total_appointments}</span>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="stat-card">
|
|
86
|
+
<span className="stat-label">Completed</span>
|
|
87
|
+
<span className="stat-value">{report.total_completed}</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div className="stat-card">
|
|
90
|
+
<span className="stat-label">Cancelled</span>
|
|
91
|
+
<span className="stat-value">{report.total_cancelled}</span>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="stat-card">
|
|
94
|
+
<span className="stat-label">Patients Attended</span>
|
|
95
|
+
<span className="stat-value">{report.patients_attended?.length || 0}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="card">
|
|
100
|
+
<h2>
|
|
101
|
+
Patients Attended ({report.start_date} to {report.end_date})
|
|
102
|
+
</h2>
|
|
103
|
+
{report.patients_attended?.length === 0 ? (
|
|
104
|
+
<p className="text-muted">No patients attended in this period.</p>
|
|
105
|
+
) : (
|
|
106
|
+
<div className="table-responsive">
|
|
107
|
+
<table>
|
|
108
|
+
<thead>
|
|
109
|
+
<tr>
|
|
110
|
+
<th>Name</th>
|
|
111
|
+
<th>Gender</th>
|
|
112
|
+
<th>Phone</th>
|
|
113
|
+
<th>Report Date</th>
|
|
114
|
+
</tr>
|
|
115
|
+
</thead>
|
|
116
|
+
<tbody>
|
|
117
|
+
{report.patients_attended.map((p) => (
|
|
118
|
+
<tr key={`${p.id}-${p.report_date}`}>
|
|
119
|
+
<td>{p.full_name}</td>
|
|
120
|
+
<td>{p.gender}</td>
|
|
121
|
+
<td>{p.phone_number}</td>
|
|
122
|
+
<td>{p.report_date?.split('T')[0]}</td>
|
|
123
|
+
</tr>
|
|
124
|
+
))}
|
|
125
|
+
</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div className="card">
|
|
132
|
+
<h2>Appointments in Period</h2>
|
|
133
|
+
{report.appointments?.length === 0 ? (
|
|
134
|
+
<p className="text-muted">No appointments in this period.</p>
|
|
135
|
+
) : (
|
|
136
|
+
<div className="table-responsive">
|
|
137
|
+
<table>
|
|
138
|
+
<thead>
|
|
139
|
+
<tr>
|
|
140
|
+
<th>Patient</th>
|
|
141
|
+
<th>Doctor</th>
|
|
142
|
+
<th>Date</th>
|
|
143
|
+
<th>Time</th>
|
|
144
|
+
<th>Status</th>
|
|
145
|
+
</tr>
|
|
146
|
+
</thead>
|
|
147
|
+
<tbody>
|
|
148
|
+
{report.appointments.map((a) => (
|
|
149
|
+
<tr key={a.id}>
|
|
150
|
+
<td>{a.patient_name}</td>
|
|
151
|
+
<td>{a.doctor_name}</td>
|
|
152
|
+
<td>{a.appointment_date?.split('T')[0]}</td>
|
|
153
|
+
<td>{a.appointment_time?.slice(0, 5)}</td>
|
|
154
|
+
<td>
|
|
155
|
+
<span className={`status-badge status-${a.status}`}>
|
|
156
|
+
{a.status}
|
|
157
|
+
</span>
|
|
158
|
+
</td>
|
|
159
|
+
</tr>
|
|
160
|
+
))}
|
|
161
|
+
</tbody>
|
|
162
|
+
</table>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const api = axios.create({
|
|
4
|
+
baseURL: process.env.REACT_APP_API_URL || '/api',
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
api.interceptors.request.use((config) => {
|
|
8
|
+
const token = localStorage.getItem('token');
|
|
9
|
+
if (token) {
|
|
10
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
11
|
+
}
|
|
12
|
+
return config;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export default api;
|
package/apps/config.js
ADDED
package/exclude.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
package/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cpSync, existsSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { cwd } from "process";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
import kleur from "kleur";
|
|
9
|
+
import { apps } from "./apps/config.js";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
console.log(kleur.bold().cyan("\nš” Steve Rogers Installer\n"));
|
|
15
|
+
|
|
16
|
+
const choices = apps.map((app) => ({
|
|
17
|
+
title: `${app.title} ${kleur.gray(app.description)}`,
|
|
18
|
+
value: app.value,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const { selected } = await prompts({
|
|
22
|
+
type: "multiselect",
|
|
23
|
+
name: "selected",
|
|
24
|
+
message: "Which apps do you want to install?",
|
|
25
|
+
choices,
|
|
26
|
+
hint: "- Space to select, Return to submit",
|
|
27
|
+
min: 1,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!selected || selected.length === 0) {
|
|
31
|
+
console.log(kleur.yellow("\nNo apps selected. Exiting.\n"));
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const value of selected) {
|
|
36
|
+
const app = apps.find((a) => a.value === value);
|
|
37
|
+
const src = join(__dirname, "apps", app.folder);
|
|
38
|
+
const dest = join(cwd(), app.folder);
|
|
39
|
+
|
|
40
|
+
console.log(kleur.cyan(`\nā Installing ${app.title}...`));
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
cpSync(src, dest, { recursive: true });
|
|
44
|
+
console.log(kleur.gray(" Running npm install..."));
|
|
45
|
+
execSync("npm install", { cwd: dest, stdio: "inherit" });
|
|
46
|
+
console.log(kleur.green(` ā ${app.title} ready at ./${app.folder}\n`));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.log(kleur.red(` ā Failed: ${err.message}\n`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(kleur.bold().green("š Done!\n"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-steve-rogers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Steve Rogers personal app installer",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-steve-rogers": "index.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"kleur": "^4.1.5",
|
|
11
|
+
"prompts": "^2.4.2",
|
|
12
|
+
"unzipper": "^0.12.3"
|
|
13
|
+
}
|
|
14
|
+
}
|