[
  {
    "path": ".dockerignore",
    "content": "node_modules"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\n\n.DS_Store\n.certs/\n.idea/\n.settings/\n.build/\nlogs/access.log\n\n*.iml\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"gitdoc.enabled\": false\n}"
  },
  {
    "path": "README.md",
    "content": "# Node.js with MongoDB and Docker Demo\n\nApplication demo designed to show how Node.js and MongoDB can be run in Docker containers. \nThe app uses Mongoose to create a simple database that stores Docker commands and examples. \n\nInterested in learning more about Docker? Visit https://www.pluralsight.com/courses/docker-web-development to view my Docker for Web Developers course.\n\n### Starting the Application with Docker Containers:\n\n1. Install [Docker Desktop](https://docker.com/get-started)\n\n2. Open a command prompt.\n\n3. Run the commands listed in `node.dockerfile` (see the comments at the top of the file).\n\n4. Navigate to http://localhost:3000.\n\n\n### Starting the Application with Docker Compose\n\n1. Install [Docker Desktop](https://docker.com/get-started)\n\n2. Open a command prompt at the root of the application's folder.\n\n3. Run `docker compose build`\n\n4. Run `docker compose up`\n\n5. Open another command prompt and run `docker compose ps -a` and note the ID of the Node container\n\n6. Run `docker exec -it <nodeContainerID> sh` (replace <nodeContainerID> with the proper ID) to sh into the container\n\n7. Run `node dbSeeder.js` to seed the MongoDB database\n\n8. Type `exit` to leave the sh session\n\n9. Navigate to http://localhost:3000 in your browser to view the site. This assumes that's the IP assigned to VirtualBox - change if needed.\n\n10. Run `docker-compose down` to stop the containers and remove them.\n\n## To run the app with Node.js and MongoDB (without Docker):\n\n1. Install and start MongoDB (https://docs.mongodb.com/manual/administration/install-community/).\n\n2. Install the LTS version of Node.js (http://nodejs.org).\n\n3. Open `config/config.development.json` and adjust the host name to your MongoDB server name (`localhost` normally works if you're running locally). \n\n4. Run `npm install`.\n\n5. Run `node dbSeeder.js` to get the sample data loaded into MongoDB. Exit the command prompt.\n\n6. Run `npm start` to start the server.\n\n7. Navigate to http://localhost:3000 in your browser.\n\n\n\n\n"
  },
  {
    "path": "config/config.development.json",
    "content": "{\r\n    \"databaseConfig\": {\r\n        \"host\": \"mongodb\",\r\n        \"database\": \"funWithDocker\"\r\n    }\r\n}"
  },
  {
    "path": "config/config.production.json",
    "content": "{\r\n    \"databaseConfig\": {\r\n        \"host\": \"mongodb\",\r\n        \"database\": \"funWithDocker\"\r\n    }\r\n}"
  },
  {
    "path": "dbSeeder.js",
    "content": "import dataInitializer from './lib/dataSeeder.js';\r\nimport config from './config/config.development.json' with { type: 'json' };\r\nimport db from './lib/database.js';\r\n\r\ndb.init(config.databaseConfig);\r\n\r\nconsole.log('Initializing Data');\r\ndataInitializer.initializeData(function(err) {\r\n  if (err) {\r\n      console.log(err);\r\n  }\r\n  else {\r\n      console.log('Data Initialized!')\r\n  }\r\n});"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n\n  node:\n    container_name: nodeapp\n    image: nodeapp\n    build:\n      context: .\n      dockerfile: node.dockerfile\n      args:\n        PACKAGES: \"nano wget curl\"\n    ports:\n      - \"3000:3000\"\n    networks:\n      - nodeapp-network\n    volumes:\n      - ./logs:/var/www/logs\n    environment:\n      - NODE_ENV=production\n      - APP_VERSION=1.0\n    depends_on: \n      - mongodb\n    # restart: on-failure\n    # deploy:\n    #   replicas: 5\n      \n  mongodb:\n    container_name: mongodb\n    image: mongo\n    networks:\n      - nodeapp-network\n\nnetworks:\n  nodeapp-network:\n    driver: bridge"
  },
  {
    "path": "lib/configLoader.js",
    "content": "import { readFileSync } from 'fs';\r\nimport { fileURLToPath } from 'url';\r\nimport path from 'path';\r\nimport logger from './logger.js';\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\nlet env = process.env.NODE_ENV;\r\n\r\nif (!env) {\r\n  env = 'development';\r\n} \r\n\r\nlogger.log('Node environment: ' + env);\r\nlogger.log('loading config.' + env + '.json');\r\n\r\nconst configPath = path.join(__dirname, '..', 'config', `config.${env}.json`);\r\nconst config = JSON.parse(readFileSync(configPath, 'utf-8'));\r\n\r\nexport default config;"
  },
  {
    "path": "lib/dataSeeder.js",
    "content": "import DockerCommand from '../models/dockerCommand.js';\r\n\r\nconst dataInitializer = {\r\n  initializeData: async function(callback) {\r\n    const runDockerCommand = new DockerCommand({\r\n      command: 'run',\r\n      description: 'Runs a Docker container',\r\n      examples: [\r\n        {\r\n          example: 'docker run imageName',\r\n          description: 'Creates a running container from the image. Pulls it from Docker Hub if the image is not local'\r\n        },\r\n        {\r\n          example: 'docker run -d -p 8080:3000 imageName',\r\n          description: 'Runs a container in \"detached\" mode with an external port of 8080 and a container port of 3000.'\r\n        }\r\n      ]\r\n    });\r\n\r\n    const psDockerCommand = new DockerCommand({\r\n      command: 'ps',\r\n      description: 'Lists containers',\r\n      examples: [\r\n        {\r\n          example: 'docker ps',\r\n          description: 'Lists all running containers'\r\n        },\r\n        {\r\n          example: 'docker ps -a',\r\n          description: 'Lists all containers (even if they are not running)'\r\n        }\r\n      ]\r\n    });\r\n\r\n    try {\r\n      await runDockerCommand.save();\r\n      await psDockerCommand.save();\r\n      callback();\r\n    } catch (err) {\r\n      callback(err);\r\n    }\r\n  }\r\n};\r\n\r\nexport default dataInitializer;"
  },
  {
    "path": "lib/database.js",
    "content": "import mongoose from 'mongoose';\r\n\r\nconst database = {\r\n  conn: null,\r\n\r\n  init: function(config) {\r\n    console.log('Trying to connect to ' + config.host + '/' + config.database + ' MongoDB database');\r\n    const connString = `mongodb://${config.host}/${config.database}`;\r\n    mongoose.connect(connString);\r\n    this.conn = mongoose.connection;\r\n    this.conn.on('error', console.error.bind(console, 'connection error:'));\r\n    this.conn.once('open', () => console.log('db connection open'));\r\n    return this.conn;\r\n  },\r\n\r\n  close: function() {\r\n    if (this.conn) {\r\n      this.conn.close(() => {\r\n        console.log('Mongoose default connection disconnected through app termination');\r\n        process.exit(0);\r\n      });\r\n    }\r\n  }\r\n};\r\n\r\nexport default database;\r\n"
  },
  {
    "path": "lib/dockerCommandsRepository.js",
    "content": "import DockerCommand from '../models/dockerCommand.js';\r\n\r\nconst getDockerCommands = async () => {\r\n  try {\r\n    const commands = await DockerCommand.find();\r\n    return commands;\r\n  }\r\n  catch (err) {\r\n    return [];\r\n  }\r\n};\r\n\r\nexport default getDockerCommands;"
  },
  {
    "path": "lib/logger.js",
    "content": "const logger = {\r\n  log: function(msg) {\r\n    if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {\r\n      console.log(msg);\r\n    }\r\n  }\r\n};\r\n\r\nexport default logger;"
  },
  {
    "path": "logs/.gitkeep",
    "content": ""
  },
  {
    "path": "models/dockerCommand.js",
    "content": "import mongoose from 'mongoose';\r\n\r\nconst Schema = mongoose.Schema;\r\nconst ObjectId = Schema.Types.ObjectId;\r\n    \r\nconst exampleSchema = new Schema({\r\n  example: { type: String, required: true },\r\n  description: { type: String, required: true },\r\n});\r\n\r\nconst dockerCommandSchema = new Schema({\r\n  command: { type: String, required: true },\r\n  description: { type: String, required: true },\r\n  examples: [exampleSchema]\r\n});\r\n\r\nconst DockerCommandModel = mongoose.model('dockerCommand', dockerCommandSchema);\r\n\r\nexport default DockerCommandModel;"
  },
  {
    "path": "nginx/index.html",
    "content": "<html>\n    <body>\n        Hello from a file stored in a volume!\n    </body>\n</html>"
  },
  {
    "path": "node.dockerfile",
    "content": "FROM        node:alpine\n\nLABEL       author=\"Dan Wahlin\"\nARG         PACKAGES=nano\n\nENV         NODE_ENV=production\nENV         PORT=3000\nENV         TERM=xterm\n\nRUN         apk update && apk add --no-cache $PACKAGES\n\nWORKDIR     /var/www\n\nCOPY        package*.json ./\nRUN         npm ci --only=production && npm cache clean --force\n\nCOPY        . ./\n\nEXPOSE      $PORT\n\nENTRYPOINT  [\"npm\", \"start\"]\n\n# Build: docker build -f node.dockerfile -t nodeapp .\n\n# Option 1: Create a custom bridge network and add containers into it\n\n## Create a user-defined bridge network and add containers\n# docker network create --driver bridge isolated_network\n# docker run -d --network=isolated_network --name mongodb mongo\n\n## NOTE: Use $(pwd) on macOS/Linux, ${PWD} in PowerShell, and %cd% in Windows CMD.\n# macOS/Linux\n# docker run -d --network=isolated_network --name nodeapp -p 3000:3000 -v \"$(pwd)\"/logs:/var/www/logs nodeapp\n\n# PowerShell\n# docker run -d --network=isolated_network --name nodeapp -p 3000:3000 -v ${PWD}/logs:/var/www/logs nodeapp\n\n# Option 2 (Legacy Linking - this is the OLD way)\n# Start MongoDB and Node (link Node to MongoDB container with legacy linking)\n \n# docker run -d --name my-mongodb mongo\n# docker run -d -p 3000:3000 --link my-mongodb:mongodb --name nodeapp danwahlin/nodeapp\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"expressite\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"node server.js\",\n    \"tailwind:css\": \"tailwindcss -i public/styles/tailwind.css -o public/styles/styles.css\"\n  },\n  \"dependencies\": {\n    \"@handlebars/allow-prototype-access\": \"^1.0.5\",\n    \"cookie-parser\": \"^1.4.7\",\n    \"debug\": \"^4.4.3\",\n    \"express\": \"5.1.0\",\n    \"express-handlebars\": \"8.0.3\",\n    \"handlebars\": \"^4.7.8\",\n    \"mongoose\": \"^8.19.2\",\n    \"morgan\": \"^1.10.1\",\n    \"readline\": \"1.3.0\",\n    \"serve-favicon\": \"^2.5.1\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/cli\": \"^4.1.16\",\n    \"tailwindcss\": \"4.1.16\"\n  }\n}\n"
  },
  {
    "path": "public/styles/styles.css",
    "content": "/*! tailwindcss v4.1.14 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n  :root, :host {\n    --font-sans: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\",\n      \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n      \"Courier New\", monospace;\n    --color-blue-500: oklch(62.3% 0.214 259.815);\n    --color-gray-300: oklch(87.2% 0.01 258.338);\n    --color-gray-400: oklch(70.7% 0.022 261.325);\n    --color-gray-700: oklch(37.3% 0.034 259.733);\n    --color-gray-800: oklch(27.8% 0.033 256.848);\n    --color-gray-900: oklch(21% 0.034 264.665);\n    --color-white: #fff;\n    --spacing: 0.25rem;\n    --container-7xl: 80rem;\n    --text-sm: 0.875rem;\n    --text-sm--line-height: calc(1.25 / 0.875);\n    --text-base: 1rem;\n    --text-base--line-height: calc(1.5 / 1);\n    --text-lg: 1.125rem;\n    --text-lg--line-height: calc(1.75 / 1.125);\n    --font-weight-medium: 500;\n    --font-weight-bold: 700;\n    --radius-md: 0.375rem;\n    --default-font-family: var(--font-sans);\n    --default-mono-font-family: var(--font-mono);\n  }\n}\n@layer base {\n  *, ::after, ::before, ::backdrop, ::file-selector-button {\n    box-sizing: border-box;\n    margin: 0;\n    padding: 0;\n    border: 0 solid;\n  }\n  html, :host {\n    line-height: 1.5;\n    -webkit-text-size-adjust: 100%;\n    tab-size: 4;\n    font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n    font-feature-settings: var(--default-font-feature-settings, normal);\n    font-variation-settings: var(--default-font-variation-settings, normal);\n    -webkit-tap-highlight-color: transparent;\n  }\n  hr {\n    height: 0;\n    color: inherit;\n    border-top-width: 1px;\n  }\n  abbr:where([title]) {\n    -webkit-text-decoration: underline dotted;\n    text-decoration: underline dotted;\n  }\n  h1, h2, h3, h4, h5, h6 {\n    font-size: inherit;\n    font-weight: inherit;\n  }\n  a {\n    color: inherit;\n    -webkit-text-decoration: inherit;\n    text-decoration: inherit;\n  }\n  b, strong {\n    font-weight: bolder;\n  }\n  code, kbd, samp, pre {\n    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);\n    font-feature-settings: var(--default-mono-font-feature-settings, normal);\n    font-variation-settings: var(--default-mono-font-variation-settings, normal);\n    font-size: 1em;\n  }\n  small {\n    font-size: 80%;\n  }\n  sub, sup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n  }\n  sub {\n    bottom: -0.25em;\n  }\n  sup {\n    top: -0.5em;\n  }\n  table {\n    text-indent: 0;\n    border-color: inherit;\n    border-collapse: collapse;\n  }\n  :-moz-focusring {\n    outline: auto;\n  }\n  progress {\n    vertical-align: baseline;\n  }\n  summary {\n    display: list-item;\n  }\n  ol, ul, menu {\n    list-style: none;\n  }\n  img, svg, video, canvas, audio, iframe, embed, object {\n    display: block;\n    vertical-align: middle;\n  }\n  img, video {\n    max-width: 100%;\n    height: auto;\n  }\n  button, input, select, optgroup, textarea, ::file-selector-button {\n    font: inherit;\n    font-feature-settings: inherit;\n    font-variation-settings: inherit;\n    letter-spacing: inherit;\n    color: inherit;\n    border-radius: 0;\n    background-color: transparent;\n    opacity: 1;\n  }\n  :where(select:is([multiple], [size])) optgroup {\n    font-weight: bolder;\n  }\n  :where(select:is([multiple], [size])) optgroup option {\n    padding-inline-start: 20px;\n  }\n  ::file-selector-button {\n    margin-inline-end: 4px;\n  }\n  ::placeholder {\n    opacity: 1;\n  }\n  @supports (not (-webkit-appearance: -apple-pay-button))  or (contain-intrinsic-size: 1px) {\n    ::placeholder {\n      color: currentcolor;\n      @supports (color: color-mix(in lab, red, red)) {\n        color: color-mix(in oklab, currentcolor 50%, transparent);\n      }\n    }\n  }\n  textarea {\n    resize: vertical;\n  }\n  ::-webkit-search-decoration {\n    -webkit-appearance: none;\n  }\n  ::-webkit-date-and-time-value {\n    min-height: 1lh;\n    text-align: inherit;\n  }\n  ::-webkit-datetime-edit {\n    display: inline-flex;\n  }\n  ::-webkit-datetime-edit-fields-wrapper {\n    padding: 0;\n  }\n  ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n    padding-block: 0;\n  }\n  ::-webkit-calendar-picker-indicator {\n    line-height: 1;\n  }\n  :-moz-ui-invalid {\n    box-shadow: none;\n  }\n  button, input:where([type=\"button\"], [type=\"reset\"], [type=\"submit\"]), ::file-selector-button {\n    appearance: button;\n  }\n  ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n    height: auto;\n  }\n  [hidden]:where(:not([hidden=\"until-found\"])) {\n    display: none !important;\n  }\n}\n@layer utilities {\n  .sr-only {\n    position: absolute;\n    width: 1px;\n    height: 1px;\n    padding: 0;\n    margin: -1px;\n    overflow: hidden;\n    clip-path: inset(50%);\n    white-space: nowrap;\n    border-width: 0;\n  }\n  .container {\n    width: 100%;\n    @media (width >= 40rem) {\n      max-width: 40rem;\n    }\n    @media (width >= 48rem) {\n      max-width: 48rem;\n    }\n    @media (width >= 64rem) {\n      max-width: 64rem;\n    }\n    @media (width >= 80rem) {\n      max-width: 80rem;\n    }\n    @media (width >= 96rem) {\n      max-width: 96rem;\n    }\n  }\n  .mx-auto {\n    margin-inline: auto;\n  }\n  .-mr-2 {\n    margin-right: calc(var(--spacing) * -2);\n  }\n  .mb-2 {\n    margin-bottom: calc(var(--spacing) * 2);\n  }\n  .mb-6 {\n    margin-bottom: calc(var(--spacing) * 6);\n  }\n  .ml-2 {\n    margin-left: calc(var(--spacing) * 2);\n  }\n  .ml-10 {\n    margin-left: calc(var(--spacing) * 10);\n  }\n  .block {\n    display: block;\n  }\n  .flex {\n    display: flex;\n  }\n  .hidden {\n    display: none;\n  }\n  .inline-flex {\n    display: inline-flex;\n  }\n  .h-6 {\n    height: calc(var(--spacing) * 6);\n  }\n  .h-9 {\n    height: calc(var(--spacing) * 9);\n  }\n  .h-10 {\n    height: calc(var(--spacing) * 10);\n  }\n  .h-16 {\n    height: calc(var(--spacing) * 16);\n  }\n  .w-6 {\n    width: calc(var(--spacing) * 6);\n  }\n  .w-10 {\n    width: calc(var(--spacing) * 10);\n  }\n  .w-14 {\n    width: calc(var(--spacing) * 14);\n  }\n  .w-full {\n    width: 100%;\n  }\n  .max-w-7xl {\n    max-width: var(--container-7xl);\n  }\n  .flex-shrink-0 {\n    flex-shrink: 0;\n  }\n  .flex-col {\n    flex-direction: column;\n  }\n  .items-baseline {\n    align-items: baseline;\n  }\n  .items-center {\n    align-items: center;\n  }\n  .justify-between {\n    justify-content: space-between;\n  }\n  .justify-center {\n    justify-content: center;\n  }\n  .space-y-1 {\n    :where(& > :not(:last-child)) {\n      --tw-space-y-reverse: 0;\n      margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));\n      margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));\n    }\n  }\n  .space-y-4 {\n    :where(& > :not(:last-child)) {\n      --tw-space-y-reverse: 0;\n      margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));\n      margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));\n    }\n  }\n  .space-x-4 {\n    :where(& > :not(:last-child)) {\n      --tw-space-x-reverse: 0;\n      margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));\n      margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));\n    }\n  }\n  .rounded {\n    border-radius: 0.25rem;\n  }\n  .rounded-md {\n    border-radius: var(--radius-md);\n  }\n  .border {\n    border-style: var(--tw-border-style);\n    border-width: 1px;\n  }\n  .border-gray-300 {\n    border-color: var(--color-gray-300);\n  }\n  .bg-gray-800 {\n    background-color: var(--color-gray-800);\n  }\n  .bg-gray-900 {\n    background-color: var(--color-gray-900);\n  }\n  .bg-white {\n    background-color: var(--color-white);\n  }\n  .p-2 {\n    padding: calc(var(--spacing) * 2);\n  }\n  .px-2 {\n    padding-inline: calc(var(--spacing) * 2);\n  }\n  .px-3 {\n    padding-inline: calc(var(--spacing) * 3);\n  }\n  .px-4 {\n    padding-inline: calc(var(--spacing) * 4);\n  }\n  .py-2 {\n    padding-block: calc(var(--spacing) * 2);\n  }\n  .py-4 {\n    padding-block: calc(var(--spacing) * 4);\n  }\n  .py-6 {\n    padding-block: calc(var(--spacing) * 6);\n  }\n  .pt-2 {\n    padding-top: calc(var(--spacing) * 2);\n  }\n  .pb-3 {\n    padding-bottom: calc(var(--spacing) * 3);\n  }\n  .font-sans {\n    font-family: var(--font-sans);\n  }\n  .text-base {\n    font-size: var(--text-base);\n    line-height: var(--tw-leading, var(--text-base--line-height));\n  }\n  .text-lg {\n    font-size: var(--text-lg);\n    line-height: var(--tw-leading, var(--text-lg--line-height));\n  }\n  .text-sm {\n    font-size: var(--text-sm);\n    line-height: var(--tw-leading, var(--text-sm--line-height));\n  }\n  .font-bold {\n    --tw-font-weight: var(--font-weight-bold);\n    font-weight: var(--font-weight-bold);\n  }\n  .font-medium {\n    --tw-font-weight: var(--font-weight-medium);\n    font-weight: var(--font-weight-medium);\n  }\n  .text-gray-300 {\n    color: var(--color-gray-300);\n  }\n  .text-gray-400 {\n    color: var(--color-gray-400);\n  }\n  .text-gray-900 {\n    color: var(--color-gray-900);\n  }\n  .text-white {\n    color: var(--color-white);\n  }\n  .no-underline {\n    text-decoration-line: none;\n  }\n  .shadow {\n    --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n  }\n  .shadow-sm {\n    --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n  }\n  .hover\\:bg-gray-700 {\n    &:hover {\n      @media (hover: hover) {\n        background-color: var(--color-gray-700);\n      }\n    }\n  }\n  .hover\\:text-white {\n    &:hover {\n      @media (hover: hover) {\n        color: var(--color-white);\n      }\n    }\n  }\n  .focus\\:border-blue-500 {\n    &:focus {\n      border-color: var(--color-blue-500);\n    }\n  }\n  .focus\\:ring-2 {\n    &:focus {\n      --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n      box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n    }\n  }\n  .focus\\:ring-blue-500 {\n    &:focus {\n      --tw-ring-color: var(--color-blue-500);\n    }\n  }\n  .focus\\:ring-white {\n    &:focus {\n      --tw-ring-color: var(--color-white);\n    }\n  }\n  .focus\\:ring-offset-2 {\n    &:focus {\n      --tw-ring-offset-width: 2px;\n      --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n    }\n  }\n  .focus\\:ring-offset-gray-800 {\n    &:focus {\n      --tw-ring-offset-color: var(--color-gray-800);\n    }\n  }\n  .focus\\:outline-none {\n    &:focus {\n      --tw-outline-style: none;\n      outline-style: none;\n    }\n  }\n  .sm\\:px-3 {\n    @media (width >= 40rem) {\n      padding-inline: calc(var(--spacing) * 3);\n    }\n  }\n  .sm\\:px-6 {\n    @media (width >= 40rem) {\n      padding-inline: calc(var(--spacing) * 6);\n    }\n  }\n  .md\\:block {\n    @media (width >= 48rem) {\n      display: block;\n    }\n  }\n  .md\\:hidden {\n    @media (width >= 48rem) {\n      display: none;\n    }\n  }\n  .lg\\:px-8 {\n    @media (width >= 64rem) {\n      padding-inline: calc(var(--spacing) * 8);\n    }\n  }\n}\n@property --tw-space-y-reverse {\n  syntax: \"*\";\n  inherits: false;\n  initial-value: 0;\n}\n@property --tw-space-x-reverse {\n  syntax: \"*\";\n  inherits: false;\n  initial-value: 0;\n}\n@property --tw-border-style {\n  syntax: \"*\";\n  inherits: false;\n  initial-value: solid;\n}\n@property --tw-font-weight {\n  syntax: \"*\";\n  inherits: false;\n}\n@property --tw-shadow {\n  syntax: \"*\";\n  inherits: false;\n  initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n  syntax: \"*\";\n  inherits: false;\n}\n@property --tw-shadow-alpha {\n  syntax: \"<percentage>\";\n  inherits: false;\n  initial-value: 100%;\n}\n@property --tw-inset-shadow {\n  syntax: \"*\";\n  inherits: false;\n  initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n  syntax: \"*\";\n  inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n  syntax: \"<percentage>\";\n  inherits: false;\n  initial-value: 100%;\n}\n@property --tw-ring-color {\n  syntax: \"*\";\n  inherits: false;\n}\n@property --tw-ring-shadow {\n  syntax: \"*\";\n  inherits: false;\n  initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n  syntax: \"*\";\n  inherits: false;\n}\n@property --tw-inset-ring-shadow {\n  syntax: \"*\";\n  inherits: false;\n  initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n  syntax: \"*\";\n  inherits: false;\n}\n@property --tw-ring-offset-width {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n  syntax: \"*\";\n  inherits: false;\n  initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n  syntax: \"*\";\n  inherits: false;\n  initial-value: 0 0 #0000;\n}\n@layer properties {\n  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n    *, ::before, ::after, ::backdrop {\n      --tw-space-y-reverse: 0;\n      --tw-space-x-reverse: 0;\n      --tw-border-style: solid;\n      --tw-font-weight: initial;\n      --tw-shadow: 0 0 #0000;\n      --tw-shadow-color: initial;\n      --tw-shadow-alpha: 100%;\n      --tw-inset-shadow: 0 0 #0000;\n      --tw-inset-shadow-color: initial;\n      --tw-inset-shadow-alpha: 100%;\n      --tw-ring-color: initial;\n      --tw-ring-shadow: 0 0 #0000;\n      --tw-inset-ring-color: initial;\n      --tw-inset-ring-shadow: 0 0 #0000;\n      --tw-ring-inset: initial;\n      --tw-ring-offset-width: 0px;\n      --tw-ring-offset-color: #fff;\n      --tw-ring-offset-shadow: 0 0 #0000;\n    }\n  }\n}\n"
  },
  {
    "path": "public/styles/tailwind.css",
    "content": "@import \"tailwindcss\";"
  },
  {
    "path": "routes/index.js",
    "content": "import express from 'express';\nimport getDockerCommands from '../lib/dockerCommandsRepository.js';\nimport DockerCommandModel from '../models/dockerCommand.js';\n\nconst router = express.Router();\n\n/* GET home page. */\nrouter.get('/', async (req, res, next) => {\n  const commands = await getDockerCommands();\n  res.render('index', { dockerCommands: commands });\n});\n\n/* GET new command page */\nrouter.get('/newcommand', (req, res, next) => {\n  res.render('newcommand');\n});\n\nrouter.post('/newcommand', async (req, res, next) => {\n  // Extremely simple implementation to get a command in the database\n  const commandData = {\n    command: req.body.command,\n    description: req.body.description,\n    examples: [{\n      example: req.body.example,\n      description: req.body.ex_description\n    }]\n  }\n  const command = new DockerCommandModel(commandData);\n  try {\n    const cmd = await command.save();\n    console.log(cmd.command + \" saved to commands collection.\");\n  }\n  catch (err) {\n    console.log(err);\n  }\n  res.redirect('/');\n});\n\nexport default router;\n"
  },
  {
    "path": "server.js",
    "content": "import express from 'express';\nimport exphbs from 'express-handlebars';\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport favicon from 'serve-favicon';\nimport morgan from 'morgan';\nimport cookieParser from 'cookie-parser';\nimport Handlebars from 'handlebars';\nimport { allowInsecurePrototypeAccess } from '@handlebars/allow-prototype-access';\nimport config from './lib/configLoader.js';\nimport db from './lib/database.js';\nimport routes from './routes/index.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst port = process.env.PORT || 3000;\nconst app = express();\n\n// view engine setup\nconst hbs = exphbs.create({\n    extname: '.hbs',\n    defaultLayout: 'masterLayout',\n    // https://www.npmjs.com/package/@handlebars/allow-prototype-access\n    // Need to add due to security change in Handlebars 4.6+\n    handlebars:  allowInsecurePrototypeAccess(Handlebars)\n});\napp.engine('hbs', hbs.engine);\napp.set('view engine', 'hbs');\napp.set('views', path.join(__dirname, 'views'));\n\n// create a write stream (in append mode)\nconst accessLogStream = fs.createWriteStream(path.join(__dirname, '/logs/access.log'), { flags: 'a' });\napp.use(morgan('combined', { stream: accessLogStream }));\n\napp.use(favicon(__dirname + '/public/images/favicon.ico'));\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\napp.use(cookieParser());\napp.use(express.static(path.join(__dirname, 'public')));\n\n//Pass database config settings\ndb.init(config.databaseConfig);\n\napp.use('/', routes);\n\n// catch 404 and forward to error handler\napp.use((req, res, next) => {\n  var err = new Error('Not Found');\n  err.status = 404;\n  next(err);\n});\n\n// error handlers\n\n// development error handler\n// will print stacktrace\nif (app.get('env') === 'development') {\n  app.use((err, req, res, next) => {\n    res.status(err.status || 500);\n    res.render('error', {\n      message: err.message,\n      error: err\n    });\n  });\n}\n\n// production error handler\n// no stacktraces leaked to user\napp.use((err, req, res, next) => {\n  res.status(err.status || 500);\n  res.render('error', {\n    message: err.message,\n    error: {}\n  });\n});\n\napp.listen(port, (err) => {\n    console.log('[%s] Listening on http://localhost:%d', app.settings.env, port);\n});\n\n\n//*********************************************************\n//    Quick and dirty way to detect event loop blocking\n//*********************************************************\nlet lastLoop = Date.now();\n\nfunction monitorEventLoop() {\n    const time = Date.now();\n    if (time - lastLoop > 1000) console.error('Event loop blocked ' + (time - lastLoop));\n    lastLoop = time;\n    setTimeout(monitorEventLoop, 200);\n}\n\nif (process.env.NODE_ENV === 'development') {\n    monitorEventLoop();\n}\n\nexport default app;\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "export default {\n  content: [\n    \"./views/**/*.{hbs,handlebars,html}\",\n    \"./routes/**/*.js\",\n    \"./lib/**/*.js\",\n    \"./public/scripts/**/*.js\"\n  ],\n  theme: {\n    extend: {}\n  },\n  plugins: []\n};"
  },
  {
    "path": "views/error.hbs",
    "content": "<h1>{{message}}</h1>\n<h2>{{error.status}}</h2>\n<pre>{{error.stack}}</pre>\n"
  },
  {
    "path": "views/index.hbs",
    "content": "{{#each dockerCommands}}\n   <div class=\"flex items-center\">\n      <div class=\"w-14\"><img src=\"/images/gear-set-blue.png\" class=\"h-9\"></div>\n      <div class=\"\"><h2>{{ command }} Command</h2></div>\n   </div>\n   <br />\n   {{ description }}\n   <br /><br />\n   \n   {{#each examples}}\n        <h3>{{ example }}</h3>\n        {{ description }}\n        <br />\n   {{/each}}\n   \n   <br><br>\n{{/each}}\n"
  },
  {
    "path": "views/layouts/masterLayout.hbs",
    "content": "<!DOCTYPE html>\n<html class=\"font-sans\">\n\n<head>\n  <title>{{title}}</title>\n  <link href=\"/styles/styles.css\" rel=\"stylesheet\">\n  <link rel=\"icon\" href=\"images/favicon.ico\">\n</head>\n\n<body>\n  <!-- This example requires Tailwind CSS v2.0+ -->\n  <div>\n    <nav class=\"bg-gray-800\">\n      <div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n        <div class=\"flex items-center justify-between h-16\">\n          <div class=\"flex items-center\">\n            <div class=\"flex-shrink-0\">\n              <img class=\"h-10 w-10\" src=\"/images/docker.png\" alt=\"Docker\">\n            </div>\n            <a href=\"/\" class=\"text-white hover:bg-gray-700 hover:text-white ml-2 no-underline\">\n              Building and Running Your First Docker App\n            </a>\n            <div class=\"hidden md:block\">\n              <div class=\"ml-10 flex items-baseline space-x-4\">\n                <!-- Current: \"bg-gray-900 text-white\", Default: \"text-gray-300 hover:bg-gray-700 hover:text-white\" -->\n                <a href=\"/newcommand\" class=\"bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium no-underline\">Add Command</a>\n              </div>\n            </div>\n          </div>\n        </div>\n        <div class=\"-mr-2 flex md:hidden\">\n          <!-- Mobile menu button -->\n          <button type=\"button\"\n            class=\"bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white\"\n            aria-controls=\"mobile-menu\" aria-expanded=\"false\">\n            <span class=\"sr-only\">Open main menu</span>\n            <!--\n              Heroicon name: outline/menu\n\n              Menu open: \"hidden\", Menu closed: \"block\"\n            -->\n            <svg class=\"block h-6 w-6\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"\n              stroke=\"currentColor\" aria-hidden=\"true\">\n              <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 12h16M4 18h16\" />\n            </svg>\n            <!--\n              Heroicon name: outline/x\n\n              Menu open: \"block\", Menu closed: \"hidden\"\n            -->\n            <svg class=\"hidden h-6 w-6\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"\n              stroke=\"currentColor\" aria-hidden=\"true\">\n              <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n            </svg>\n          </button>\n        </div>\n      </div>\n  </div>\n\n  <!-- Mobile menu, show/hide based on menu state. -->\n  <div class=\"md:hidden\" id=\"mobile-menu\">\n    <div class=\"px-2 pt-2 pb-3 space-y-1 sm:px-3\">\n      <a href=\"#\" class=\"bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium\">Add Command</a>\n    </div>\n  </div>\n  </nav>\n\n  <header class=\"bg-white shadow\">\n    <div class=\"max-w-7xl mx-auto py-2 px-4 sm:px-6 lg:px-8\">\n      <h1 class=\"text-gray-900\">\n        Docker Commands\n      </h1>\n    </div>\n  </header>\n  <main class=\"max-w-7xl mx-auto py-6 sm:px-6 lg:px-8\">\n    {{{body}}}\n  </main>\n  </div>\n</body>\n\n</html>"
  },
  {
    "path": "views/newcommand.hbs",
    "content": "<form class=\"flex flex-col space-y-4 mb-6\" method=\"post\">\n  <div class=\"flex flex-col\">\n    <label class=\"mb-2 font-bold text-lg\" for=\"command\">Command</label>\n    <input class=\"block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500\" type=\"text\" name=\"command\" id=\"command\">\n  </div>\n  <div class=\"flex flex-col\">\n    <label class=\"mb-2 font-bold text-lg\" for=\"description\">Description</label>\n    <textarea class=\"block w-full py-4 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500\" name=\"description\" id=\"description\"></textarea>\n  </div>\n  <div class=\"flex flex-col\">\n    <label class=\"mb-2 font-bold text-lg\" for=\"example\">Example</label>\n    <input class=\"block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500\" type=\"text\" name=\"example\" id=\"example\">\n  </div>\n  <div class=\"flex flex-col\">\n    <label class=\"mb-2 font-bold text-lg\" for=\"ex_description\">Example Description</label>\n    <input class=\"block w-full py-2 px-3 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500\" type=\"text\" name=\"ex_description\" id=\"ex_description\">\n  </div>\n  <button type=\"submit\" class=\"self-start inline-flex items-center px-4 py-2 text-lg font-semibold text-white bg-gray-800 rounded shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900\">Add Command</button>\n</form>"
  }
]