Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 56 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Full-Stack Starter
# Full Stack Starter

This repository contains a "starter" project for web application development in JavaScript. This includes the following components, from front-end to back-end:

- React 18.2.0
- React Router 6.20.0
- Bootstrap 5.3.2
- Node.js 20.11.0
- Express 4.18.2
- Sequelize 6.35.1
- Postgres 15.5
- React 18.3.1
- React Router 7.1.1
- Bootstrap 5.3.3
- Vite 6.0.7
- Fastify 5.2.0
- Prisma 6.1.0
- Node.js 22.12.0
- Postgres 17.2

## One-time Setup

Expand Down Expand Up @@ -42,10 +43,10 @@ This repository contains a "starter" project for web application development in

```
full-stack-starter-server-1 | 5:31:23 PM client.1 | VITE v4.3.9 ready in 327 ms
full-stack-starter-server-1 | 5:31:23 PM client.1 | ➜ Local: http://localhost:3000/
full-stack-starter-server-1 | 5:31:23 PM client.1 | ➜ Local: http://localhost:5000/
```

5. Now you should be able to open the web app in your browser at: http://localhost:3000/
5. Now you should be able to open the web app in your browser at: http://localhost:5000/

6. Open a new tab or window of your shell, change into your repo directory as needed, and execute this command:

Expand Down Expand Up @@ -90,6 +91,49 @@ This repository contains a "starter" project for web application development in
8. That's it! After all this setup is complete, the only command you need to run to get
started again is the `docker compose up` command.

## Development Tools

This project includes components with helpful developer tools, such as the following:

1. Mailcatcher

The Docker Compose configuration includes the Mailcatcher development mail server. Email sent from the
server will be captured by this mail server and can be viewed on the web at:

http://localhost:1080

NO live emails will be sent over the Internet.

2. Prisma Studio

The Prisma library includes a web interface for browsing the contents of the development database at:

http://localhost:5555

3. Scalar API Documentation Renderer

The Scalar library automatically generates web-based API documentation for the server based on the
Swagger/OpenAPI schema definitions included with each route, viewable at:

http://localhost:5000/api/reference

4. Minio

The Docker Compose configuration includes the Minio object storage server as a local development
simulation of AWS S3. You can browse the contents of the storage server at:

http://localhost:9001

Username and password are: minioadmin/minioadmin

## Testing

This repo includes a Github Actions workflow for running server tests. To test locally, log in
to a running server container as describe above (`docker compose exec server bash -l`) and then run
`npm test`. The server tests use the Testcontainers library to automatically launch test databases and
storage servers for testing- if tests terminate unexpectedly, you may have dangling/orphan containers
running. Use `docker ps` to list and check running containers.

## Heroku Deployment Setup

1. Sign up for a Heroku account at: https://signup.heroku.com/
Expand Down Expand Up @@ -290,8 +334,8 @@ This repository contains a "starter" project for web application development in

## License

Full-Stack Starter
Copyright (C) 2023 <Dev/Mission>
Full Stack Starter
Copyright (C) 2025 Dev/Mission

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
Expand Down
10 changes: 0 additions & 10 deletions client/.eslintrc.cjs

This file was deleted.

44 changes: 7 additions & 37 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,8 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import neostandard from 'neostandard';

export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]
export default neostandard({
ignores: [
'dist/*',
],
semi: true
});
7 changes: 2 additions & 5 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server",
"lint": "eslint .",
"lint": "eslint --fix",
"preview": "vite preview",
"test": ""
},
Expand All @@ -27,15 +27,12 @@
"react-router": "^7.1.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^9.17.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"neostandard": "^0.12.0",
"sass": "^1.83.0",
"vite": "^6.0.7"
},
Expand Down
6 changes: 3 additions & 3 deletions client/src/Admin/AdminRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Navigate, Routes, Route } from 'react-router';

import AdminUsersRoutes from './Users/AdminUsersRoutes';

function AdminRoutes() {
function AdminRoutes () {
return (
<Routes>
<Route path="users/*" element={<AdminUsersRoutes />} />
<Route path="" element={<Navigate to="users" />} />
<Route path='users/*' element={<AdminUsersRoutes />} />
<Route path='' element={<Navigate to='users' />} />
</Routes>
);
}
Expand Down
62 changes: 31 additions & 31 deletions client/src/Admin/Users/AdminUserInvite.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import UnexpectedError from '../../UnexpectedError';
import ValidationError from '../../ValidationError';
import { useStaticContext } from '../../StaticContext';

function AdminUserInvite() {
function AdminUserInvite () {
const staticContext = useStaticContext();
const navigate = useNavigate();
const [invite, setInvite] = useState({
Expand All @@ -21,13 +21,13 @@ function AdminUserInvite() {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState(null);

function onChange(event) {
function onChange (event) {
const newInvite = { ...invite };
newInvite[event.target.name] = event.target.value;
setInvite(newInvite);
}

async function onSubmit(event) {
async function onSubmit (event) {
event.preventDefault();
setLoading(true);
setError(null);
Expand All @@ -50,72 +50,72 @@ function AdminUserInvite() {
<Helmet>
<title>Invite a new User - {staticContext?.env?.VITE_SITE_TITLE ?? ''}</title>
</Helmet>
<main className="container">
<div className="row justify-content-center">
<div className="col col-sm-10 col-md-8 col-lg-6 col-xl-4">
<div className="card">
<div className="card-body">
<h2 className="card-title">Invite a new User</h2>
<main className='container'>
<div className='row justify-content-center'>
<div className='col col-sm-10 col-md-8 col-lg-6 col-xl-4'>
<div className='card'>
<div className='card-body'>
<h2 className='card-title'>Invite a new User</h2>
<form onSubmit={onSubmit}>
{error && error.message && <div className="alert alert-danger">{error.message}</div>}
{error && error.message && <div className='alert alert-danger'>{error.message}</div>}
<fieldset disabled={isLoading}>
<div className="mb-3">
<label className="form-label" htmlFor="firstName">
<div className='mb-3'>
<label className='form-label' htmlFor='firstName'>
First name
</label>
<input
type="text"
type='text'
className={classNames('form-control', { 'is-invalid': error?.errorsFor?.('firstName') })}
id="firstName"
name="firstName"
id='firstName'
name='firstName'
onChange={onChange}
value={invite.firstName ?? ''}
/>
{error?.errorMessagesHTMLFor?.('firstName')}
</div>
<div className="mb-3">
<label className="form-label" htmlFor="lastName">
<div className='mb-3'>
<label className='form-label' htmlFor='lastName'>
Last name
</label>
<input
type="text"
type='text'
className={classNames('form-control', { 'is-invalid': error?.errorsFor?.('lastName') })}
id="lastName"
name="lastName"
id='lastName'
name='lastName'
onChange={onChange}
value={invite.lastName ?? ''}
/>
{error?.errorMessagesHTMLFor?.('lastName')}
</div>
<div className="mb-3">
<label className="form-label" htmlFor="email">
<div className='mb-3'>
<label className='form-label' htmlFor='email'>
Email
</label>
<input
type="text"
type='text'
className={classNames('form-control', { 'is-invalid': error?.errorsFor?.('email') })}
id="email"
name="email"
id='email'
name='email'
onChange={onChange}
value={invite.email ?? ''}
/>
{error?.errorMessagesHTMLFor?.('email')}
</div>
<div className="mb-3">
<label className="form-label" htmlFor="message">
<div className='mb-3'>
<label className='form-label' htmlFor='message'>
Message
</label>
<textarea
className={classNames('form-control', { 'is-invalid': error?.errorsFor?.('message') })}
id="message"
name="message"
id='message'
name='message'
onChange={onChange}
value={invite.message ?? ''}
/>
{error?.errorMessagesHTMLFor?.('message')}
</div>
<div className="mb-3 d-grid">
<button className="btn btn-primary" type="submit">
<div className='mb-3 d-grid'>
<button className='btn btn-primary' type='submit'>
Submit
</button>
</div>
Expand Down
Loading
Loading