et042-mernstackapp
, choose your own namenpm init -y
git init -b dev
node_modulesdist
npm i express
import express from 'express';const app = express();const port = 3601;app.get('/', (req, res) => {res.send('testing');});app.listen(port, () => {console.log(`listening on post http://localhost:${port}`);});
"scripts": {"dev": "node ./src/index.js"},
"type": "module",
npm run dev
npm i -D typescript
npm i -D nodemon
index.js
to index.ts
npm i -D @types/express
req
and res
app.get('/', (req: express.Request, res: express.Response) => {res.send('testing TypeScript');});
req
and res
{"compilerOptions": {"module": "NodeNext","moduleResolution": "NodeNext","target": "ES2020","sourceMap": true,"outDir": "dist",},"include": ["src/**/*"]}
"nodemonConfig": {"watch": ["src"],"ext": "ts","exec": "npm run build && node dist/index.js"},
npm run dev
start nodemon, and have build run the TypeScript compiler, note that npm run build
will create transpiled JavaScript files in the dist
directory"scripts": {"dev": "nodemon","build": "tsc"},
export const getFlashcards = () => {return [{id: 1,category: "git",front: "change GitHub email",back: "git config --global user.email \"hans@nnn.com\""},{id: 2,category: "javascript",front: "count how many items in an array have a property of certain value",back: "const numberSelected = this.categories.filter(item => item.selected).length"},{id: 3,category: "html",front: "create a hyperlink",back: "<a href=\"https://www.example.com\">Click here</a>"},{id: 4,category: "css",front: "center an element horizontally",back: "margin: 0 auto;"},{id: 5,category: "python",front: "open and read a file",back: "with open('file.txt', 'r') as file:\n content = file.read()"},{id: 6,category: "javascript",front: "remove an item from an array",back: "const index = array.indexOf(item);\nif (index > -1) array.splice(index, 1);"},{id: 7,category: "sql",front: "retrieve records from a database",back: "SELECT * FROM tableName WHERE condition;"},{id: 8,category: "javascript",front: "check if a variable is an array",back: "Array.isArray(variable)"},{id: 9,category: "sql",front: "update data in a database",back: "UPDATE tableName SET column1 = value1 WHERE condition;"},{id: 10,category: "javascript",front: "use map() to transform an array",back: "const newArray = array.map(item => item * 2);"}];}
import { getFlashcards } from './model.js';app.get('/flashcards', (req: express.Request, res: express.Response) => {res.json(getFlashcards());});
app.get('/', (req: express.Request, res: express.Response) => {res.send(`<h1>Test API</h1><ul><li><a href="/flashcards">flashcards</a></li></ul>`);});
@url = http://localhost:3601### ROOT{{url}}### FLASHCARDS{{url}}/flashcards
et042-mernstackapp\et042-mernstackapp-frontend
npm create vite@latest .
code .
npm i
npm run dev
git init -b dev
<title>Flashcard Site</title>
<React.StrictMode>
(don't need this testing feature, and it causes problems later)import './App.css';function App() {return (<div><h1>Flashcard Site</h1><p>Welcome to this site.</p></div>)}export default App;
npm i -D sass
.scss
* {margin: 0;padding: 0;box-sizing: border-box;}
body {background-color: #444;color: #ddd;padding: 1rem;font-family: sans-serif;h1 {margin-bottom: 1rem;}}
npm i axios
import { useState, useEffect } from 'react';import axios from 'axios';import './App.scss';const backendUrl = 'http://localhost:3601';function App() {const [flashcards, setFlashcards] = useState([]);useEffect(() => {(async () => {const _flashcards = (await axios.get(`${backendUrl}/flashcards`)).data;setFlashcards(_flashcards);})();}, []);return (<div><h1>Flashcard Site</h1><p>There are {flashcards.length} flashcards.</p></div>)}export default App;
npm i cors
npm i -D @types/cors
(for TypeScript)import express from 'express';import { getFlashcards } from './model.js';import cors from 'cors';const app = express();app.use(cors());const port = 3601;
<p>There are {flashcards.length} flashcards.</p>{flashcards.map(flashcard => {return (<div className="flashcard"><div className="front">{flashcard.front}</div><div className="back">{flashcard.back}</div></div>)})}
div.flashcard {background-color: #222;margin-top: 1rem;padding: 1rem;div.front {color: rgb(207, 207, 118);}div.back {color: #999;font-style: italic;font-family: courier;margin-top: .5rem;font-size: .8rem;}}
npm run build
) and so cannot be deployed until you resolve the TypeScript errorsexport interface IFlashcard {id: number;category: string;front: string;back: string;}
import { IFlashcard } from './interfaces';const [flashcards, setFlashcards] = useState<IFlashcard[]>([]);
"build": "npm install && tsc","start": "node dist/index.js"
et004-render-test
npm run build
npm start
VITE_BACKEND_URL = http://localhost:3601
.env
directory to the list of files and directories for Git to ignoreconst backendUrl = import.meta.env.VITE_BACKEND_URL;
<h1>Flashcard Site</h1><p>BACKEND URL: {import.meta.env.VITE_BACKEND_URL}</p>
npm i react-icons
import { FiLoader } from 'react-icons/fi';<FiLoader className="spinner" />
<h1>Flashcard Site</h1><p className="intro">Welcome to this site. This site will enable you to test yourself on flashcards.</p>{flashcards.length === 0 ? (<FiLoader className="spinner" />) : (<><p>There are {flashcards.length} flashcards.</p>{flashcards.map(flashcard => {return (<div className="flashcard" key={flashcard.id}><div className="front">{flashcard.front}</div><div className="back">{flashcard.back}</div></div>)})}</>)}
$header-color: rgb(221, 175, 90);h1 {margin-bottom: .5rem;color: $header-color;}.intro {margin-bottom: 1.5rem;color: $header-color;}.spinner {font-size: 6rem;margin-left: .5rem;color: #888;animation: spin infinite 2s linear;}@keyframes spin {from {transform: rotate(0deg);}to {transform: rotate(360deg);}}
useEffect(() => {setTimeout(async () => {const _flashcards = (await axios.get(`${backendUrl}/flashcards`)).data;setFlashcards(_flashcards);}, 1000);}, []);
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
content: ["./index.html","./src/**/*.{js,ts,jsx,tsx}",],
@tailwind base;@tailwind components;@tailwind utilities;
font-variant: small-caps
<h1 className="text-orange-400 text-4xl">Flashcard Site</h1><p className="text-orange-300 mb-4">Welcome to this site. This site will enable you to test yourself on flashcards.</p>
<body class="bg-slate-600 p-6 text-slate-300">
<><p className="text-xl">There are {flashcards.length} flashcards.</p>{flashcards.map(flashcard => {return (<div className="bg-slate-900 p-3 mt-3 rounded-md" key={flashcard.id}><div className="text-yellow-200 text-xl">{flashcard.front}</div><div className="font-mono">{flashcard.back}</div></div>)})}</>
npm i react-router-dom
import ReactDOM from 'react-dom/client';import './index.scss';import { createBrowserRouter, RouterProvider, Outlet, NavLink, Navigate } from 'react-router-dom';const SiteHeader = () => {return (<><header><nav className="flex gap-4"><NavLink to="welcome">Welcome</NavLink><NavLink to="tech">Tech</NavLink><NavLink to="german-nouns">German Nouns</NavLink></nav></header><hr className="mt-2 mb-2" /><Outlet /></>)};const router = createBrowserRouter([{path: "/",element: <SiteHeader />,children: [{path: "/welcome",element: <div>welcome page</div>},{path: "/tech",element: <div>tech page</div>},{path: "/german-nouns",element: <div>German nouns page</div>},{index: true,element: <Navigate to="/welcome" replace />}]}])ReactDOM.createRoot(document.getElementById('root')!).render(<RouterProvider router={router} />)
import { NavLink, Outlet } from "react-router-dom";export const SiteHeader = () => {return (<><header><nav className="flex gap-4"><NavLink to="welcome">Welcome</NavLink><NavLink to="tech">Tech</NavLink><NavLink to="german-nouns">German Nouns</NavLink></nav></header><hr className="mt-2 mb-2" /><Outlet /></>)};
export const PageWelcome = () => {return (<><p>This is the welcome page.</p></>);};
const router = createBrowserRouter([{path: "/",element: <SiteHeader />,children: [{path: "/welcome",element: <PageWelcome />},{path: "/tech",element: <PageTech />},{path: "/german-nouns",element: <PageGermanNouns />},{index: true,element: <Navigate to="/welcome" replace />}]}]);
<h1 className="text-orange-400 text-4xl">Flashcard Site</h1><p className="text-orange-300 mb-4">This site enables you to create and test yourself on flashcards.</p>
import { useState, useEffect } from 'react';import axios from 'axios';import { IFlashcard } from '../interfaces';import { FiLoader } from 'react-icons/fi';const backendUrl = import.meta.env.VITE_BACKEND_URL;export const PageTech = () => {const [flashcards, setFlashcards] = useState<IFlashcard[]>([]);useEffect(() => {setTimeout(async () => {const _flashcards = (await axios.get(`${backendUrl}/flashcards`)).data;setFlashcards(_flashcards);}, 1000);}, []);return (<div>{flashcards.length === 0 ? (<FiLoader className="spinner" />) : (<><p className="text-xl">There are {flashcards.length} flashcards.</p>{flashcards.map(flashcard => {return (<div className="bg-slate-900 p-3 mt-3 rounded-md" key={flashcard.id}><div className="text-yellow-200 text-xl">{flashcard.front}</div><div className="font-mono">{flashcard.back}</div></div>)})}</>)}</div>);};
import { createContext } from 'react';interface IAppContext {testMessage: string;}interface IAppProvider {children: React.ReactNode;}export const AppContext = createContext<IAppContext>({} as IAppContext);export const AppProvider: React.FC<IAppProvider> = ({ children }) => {const testMessage = 'TEST MESSSAGE FROM APPCONTEXT';return (<AppContext.Providervalue={{testMessage}}>{children}</AppContext.Provider>);};
ReactDOM.createRoot(document.getElementById('root')!).render(<AppProvider><RouterProvider router={router} /></AppProvider>)
import { useState, useEffect, useContext } from 'react';const { testMessage } = useContext(AppContext);<h2>{testMessage}</h2>
import { createContext } from 'react';import { useState, useEffect } from 'react';import { IFlashcard } from './interfaces';import axios from 'axios';const backendUrl = import.meta.env.VITE_BACKEND_URL;interface IAppContext {flashcards: IFlashcard[];}interface IAppProvider {children: React.ReactNode;}export const AppContext = createContext<IAppContext>({} as IAppContext);export const AppProvider: React.FC<IAppProvider> = ({ children }) => {const [flashcards, setFlashcards] = useState<IFlashcard[]>([]);useEffect(() => {setTimeout(async () => {const _flashcards = (await axios.get(`${backendUrl}/flashcards`)).data;setFlashcards(_flashcards);}, 1000);}, []);return (<AppContext.Providervalue={{flashcards}}>{children}</AppContext.Provider>);};
const { flashcards } = useContext(AppContext);
import { NavLink, Outlet } from "react-router-dom";export const SiteHeader = () => {const pages = [{title: 'Welcome',idCode: 'welcome'},{title: 'Tech',idCode: 'tech'},{title: 'German Nouns',idCode: 'german-nouns'}]return (<><header><h1 className="text-blue-950 text-4xl">Flashcard Site</h1><p className="text-blue-950 mb-4 italic">This site enables you to create and test yourself on flashcards.</p><nav className="flex gap-4">{pages.map(page => {return (<NavLink to={page.idCode} className={({ isActive }) => isActive ? 'active' : ''}>{page.title}</NavLink>)})}</nav></header><hr className="mt-2 mb-2" /><Outlet /></>)};
nav a.active {color: rgb(221, 221, 101);}
<hr/>
element<nav className="flex gap-4 bg-slate-700 p-2 rounded w-fit min-w-[45rem] mb-4">
@layer components {p {@apply text-xl;}}
import { NavLink } from "react-router-dom"export const Error404 = () => {return (<><h1 className="text-4xl text-blue-950 mb-6">404 Page</h1><p>This page does not exist. Please return to <NavLink to="/welcome" className="text-yellow-200">the site</NavLink>.</p></>)}
{path: "/",errorElement: <Error404 />,element: <SiteHeader />,
{"rewrites": [{ "source": "/(.*)", "destination": "/" }]}
body {animation: fadein .5s ease-in;}@keyframes fadein {from {opacity: 0;}to {opacity: 1;}}
<style>body {background-color: #475569;}</style>