Full Code of LearnWebCode/react-course for AI

master a8f3044c6f48 cached
29 files
79.9 KB
23.5k tokens
3 symbols
1 requests
Download .txt
Repository: LearnWebCode/react-course
Branch: master
Commit: a8f3044c6f48
Files: 29
Total size: 79.9 KB

Directory structure:
gitextract_v6qlikgs/

├── .gitignore
├── 01-webpack-starter.txt
├── 02-webpack-end.txt
├── backend-api/
│   ├── Procfile
│   ├── app.js
│   ├── controllers/
│   │   ├── followController.js
│   │   ├── postController.js
│   │   └── userController.js
│   ├── db.js
│   ├── models/
│   │   ├── Follow.js
│   │   ├── Post.js
│   │   └── User.js
│   ├── package.json
│   └── router.js
├── generateHtml.js
├── html-templates/
│   ├── about.html
│   ├── chat-is-visible.html
│   ├── create-post.html
│   ├── index-empty-feed.html
│   ├── index-feed.html
│   ├── index-guest.html
│   ├── main.css
│   ├── profile-followers.html
│   ├── profile-posts.html
│   ├── search-is-visible.html
│   ├── single-post.html
│   └── terms.html
├── previewDist.js
└── vscode-react-component.txt

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.env
*.diff
*.err
*.orig
*.log
*.rej
*.swo
*.swp
*.vi
*~
*.sass-cache
.stylelintcache
node_modules/
.tmp/
yarn.lock
.DS_Store
Thumbs.db
.cache
.project
.settings
.tmproj
*.esproj
nbproject
*.sublime-project
*.sublime-workspace
*.komodoproject
.komodotools
_notes
.vscode
dwsync.xml

================================================
FILE: 01-webpack-starter.txt
================================================
const path = require("path")

module.exports = {
  entry: "./app/Main.js",
  output: {
    publicPath: "/",
    path: path.resolve(__dirname, "app"),
    filename: "bundled.js"
  },
  mode: "development",
  devtool: "source-map",
  devServer: {
    port: 3000,
    static: {
      directory: path.join(__dirname, "app")
    },
    hot: true,
    liveReload: false,
    historyApiFallback: { index: "index.html" }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-react", ["@babel/preset-env", { targets: { node: "12" } }]]
          }
        }
      }
    ]
  }
}


================================================
FILE: 02-webpack-end.txt
================================================
const currentTask = process.env.npm_lifecycle_event
const path = require("path")
const Dotenv = require("dotenv-webpack")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const HtmlWebpackHarddiskPlugin = require("html-webpack-harddisk-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const fse = require("fs-extra")

/*
  Because I didn't bother making CSS part of our
  webpack workflow for this project I'm just
  manually copying our CSS file to the DIST folder. 
*/
class RunAfterCompile {
  apply(compiler) {
    compiler.hooks.done.tap("Copy files", function () {
      fse.copySync("./app/main.css", "./dist/main.css")

      /*
        If you needed to copy another file or folder
        such as your "images" folder, you could just
        call fse.copySync() as many times as you need
        to here to cover all of your files/folders.
      */
    })
  }
}

config = {
  entry: "./app/Main.js",
  output: {
    publicPath: "/",
    path: path.resolve(__dirname, "app"),
    filename: "bundled.js",
  },
  plugins: [
    new Dotenv(),
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: "app/index-template.html",
      alwaysWriteToDisk: true,
    }),
    new HtmlWebpackHarddiskPlugin(),
  ],
  mode: "development",
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-react", ["@babel/preset-env", { targets: { node: "12" } }]],
          },
        },
      },
    ],
  },
}

if (currentTask == "webpackDev" || currentTask == "dev") {
  config.devtool = "source-map"
  config.devServer = {
    port: 3000,
    static: {
      directory: path.join(__dirname, "app")
    },
    hot: true,
    liveReload: false,
    historyApiFallback: { index: "index.html" },
  }
}

if (currentTask == "webpackBuild") {
  config.plugins.push(new CleanWebpackPlugin(), new RunAfterCompile())
  config.mode = "production"
  config.output = {
    publicPath: "/",
    path: path.resolve(__dirname, "dist"),
    filename: "[name].[chunkhash].js",
    chunkFilename: "[name].[chunkhash].js",
  }
}

module.exports = config


================================================
FILE: backend-api/Procfile
================================================
web: node db.js

================================================
FILE: backend-api/app.js
================================================
const express = require("express")
const app = express()
const sanitizeHTML = require("sanitize-html")
const jwt = require("jsonwebtoken")

app.use(express.urlencoded({ extended: false }))
app.use(express.json())

app.use("/", require("./router"))

const server = require("http").createServer(app)
const io = require("socket.io")(server, {
  pingTimeout: 30000,
  cors: true
})

io.on("connection", function(socket) {
  socket.on("chatFromBrowser", function(data) {
    try {
      let user = jwt.verify(data.token, process.env.JWTSECRET)
      socket.broadcast.emit("chatFromServer", { message: sanitizeHTML(data.message, { allowedTags: [], allowedAttributes: {} }), username: user.username, avatar: user.avatar })
    } catch (e) {
      console.log("Not a valid token for chat.")
    }
  })
})

module.exports = server


================================================
FILE: backend-api/controllers/followController.js
================================================
const Follow = require("../models/Follow")

exports.apiAddFollow = function(req, res) {
  let follow = new Follow(req.params.username, req.apiUser._id)
  follow
    .create()
    .then(() => {
      res.json(true)
    })
    .catch(errors => {
      res.json(false)
    })
}

exports.apiRemoveFollow = function(req, res) {
  let follow = new Follow(req.params.username, req.apiUser._id)
  follow
    .delete()
    .then(() => {
      res.json(true)
    })
    .catch(errors => {
      res.json(false)
    })
}


================================================
FILE: backend-api/controllers/postController.js
================================================
const Post = require("../models/Post")

exports.apiCreate = function(req, res) {
  let post = new Post(req.body, req.apiUser._id)
  post
    .create()
    .then(function(newId) {
      res.json(newId)
    })
    .catch(function(errors) {
      res.json(errors)
    })
}

exports.apiUpdate = function(req, res) {
  let post = new Post(req.body, req.apiUser._id, req.params.id)
  post
    .update()
    .then(status => {
      // the post was successfully updated in the database
      // or user did have permission, but there were validation errors
      if (status == "success") {
        res.json("success")
      } else {
        res.json("failure")
      }
    })
    .catch(e => {
      // a post with the requested id doesn't exist
      // or if the current visitor is not the owner of the requested post
      res.json("no permissions")
    })
}

exports.apiDelete = function(req, res) {
  Post.delete(req.params.id, req.apiUser._id)
    .then(() => {
      res.json("Success")
    })
    .catch(e => {
      res.json("You do not have permission to perform that action.")
    })
}

exports.search = function(req, res) {
  Post.search(req.body.searchTerm)
    .then(posts => {
      res.json(posts)
    })
    .catch(e => {
      res.json([])
    })
}

exports.reactApiViewSingle = async function(req, res) {
  try {
    let post = await Post.findSingleById(req.params.id, 0)
    res.json(post)
  } catch (e) {
    res.json(false)
  }
}


================================================
FILE: backend-api/controllers/userController.js
================================================
const User = require("../models/User")
const Post = require("../models/Post")
const Follow = require("../models/Follow")
const jwt = require("jsonwebtoken")

// how long a token lasts before expiring
const tokenLasts = "365d"

exports.apiGetPostsByUsername = async function (req, res) {
  try {
    let authorDoc = await User.findByUsername(req.params.username)
    let posts = await Post.findByAuthorId(authorDoc._id)
    //res.header("Cache-Control", "max-age=10").json(posts)
    res.json(posts)
  } catch (e) {
    res.status(500).send("Sorry, invalid user requested.")
  }
}

exports.checkToken = function (req, res) {
  try {
    req.apiUser = jwt.verify(req.body.token, process.env.JWTSECRET)
    res.json(true)
  } catch (e) {
    res.json(false)
  }
}

exports.apiMustBeLoggedIn = function (req, res, next) {
  try {
    req.apiUser = jwt.verify(req.body.token, process.env.JWTSECRET)
    next()
  } catch (e) {
    res.status(500).send("Sorry, you must provide a valid token.")
  }
}

exports.doesUsernameExist = function (req, res) {
  User.findByUsername(req.body.username.toLowerCase())
    .then(function () {
      res.json(true)
    })
    .catch(function (e) {
      res.json(false)
    })
}

exports.doesEmailExist = async function (req, res) {
  let emailBool = await User.doesEmailExist(req.body.email)
  res.json(emailBool)
}

exports.sharedProfileData = async function (req, res, next) {
  let viewerId
  try {
    viewer = jwt.verify(req.body.token, process.env.JWTSECRET)
    viewerId = viewer._id
  } catch (e) {
    viewerId = 0
  }
  req.isFollowing = await Follow.isVisitorFollowing(req.profileUser._id, viewerId)

  let postCountPromise = Post.countPostsByAuthor(req.profileUser._id)
  let followerCountPromise = Follow.countFollowersById(req.profileUser._id)
  let followingCountPromise = Follow.countFollowingById(req.profileUser._id)
  let [postCount, followerCount, followingCount] = await Promise.all([postCountPromise, followerCountPromise, followingCountPromise])

  req.postCount = postCount
  req.followerCount = followerCount
  req.followingCount = followingCount

  next()
}

exports.apiLogin = function (req, res) {
  let user = new User(req.body)
  user
    .login()
    .then(function (result) {
      res.json({
        token: jwt.sign({ _id: user.data._id, username: user.data.username, avatar: user.avatar }, process.env.JWTSECRET, { expiresIn: tokenLasts }),
        username: user.data.username,
        avatar: user.avatar
      })
    })
    .catch(function (e) {
      res.json(false)
    })
}

exports.apiRegister = function (req, res) {
  let user = new User(req.body)
  user
    .register()
    .then(() => {
      res.json({
        token: jwt.sign({ _id: user.data._id, username: user.data.username, avatar: user.avatar }, process.env.JWTSECRET, { expiresIn: tokenLasts }),
        username: user.data.username,
        avatar: user.avatar
      })
    })
    .catch(regErrors => {
      res.status(500).send(regErrors)
    })
}

exports.apiGetHomeFeed = async function (req, res) {
  try {
    let posts = await Post.getFeed(req.apiUser._id)
    res.json(posts)
  } catch (e) {
    res.status(500).send("Error")
  }
}

exports.ifUserExists = function (req, res, next) {
  User.findByUsername(req.params.username)
    .then(function (userDocument) {
      req.profileUser = userDocument
      next()
    })
    .catch(function (e) {
      res.json(false)
    })
}

exports.profileBasicData = function (req, res) {
  res.json({
    profileUsername: req.profileUser.username,
    profileAvatar: req.profileUser.avatar,
    isFollowing: req.isFollowing,
    counts: { postCount: req.postCount, followerCount: req.followerCount, followingCount: req.followingCount }
  })
}

exports.profileFollowers = async function (req, res) {
  try {
    let followers = await Follow.getFollowersById(req.profileUser._id)
    //res.header("Cache-Control", "max-age=10").json(followers)
    res.json(followers)
  } catch (e) {
    res.status(500).send("Error")
  }
}

exports.profileFollowing = async function (req, res) {
  try {
    let following = await Follow.getFollowingById(req.profileUser._id)
    //res.header("Cache-Control", "max-age=10").json(following)
    res.json(following)
  } catch (e) {
    res.status(500).send("Error")
  }
}


================================================
FILE: backend-api/db.js
================================================
const dotenv = require("dotenv")
dotenv.config()
const { MongoClient } = require("mongodb")

const client = new MongoClient(process.env.CONNECTIONSTRING)

async function start() {
  await client.connect()
  module.exports = client
  const app = require("./app")
  app.listen(process.env.PORT)
}

start()


================================================
FILE: backend-api/models/Follow.js
================================================
const usersCollection = require("../db").db().collection("users")
const followsCollection = require("../db").db().collection("follows")
const ObjectID = require("mongodb").ObjectId
const User = require("./User")

let Follow = function (followedUsername, authorId) {
  this.followedUsername = followedUsername
  this.authorId = authorId
  this.errors = []
}

Follow.prototype.cleanUp = async function () {
  if (typeof this.followedUsername != "string") {
    this.followedUsername = ""
  }
}

Follow.prototype.validate = async function (action) {
  // followedUsername must exist in database
  let followedAccount = await usersCollection.findOne({ username: this.followedUsername })
  if (followedAccount) {
    this.followedId = followedAccount._id
  } else {
    this.errors.push("You cannot follow a user that does not exist.")
  }

  let doesFollowAlreadyExist = await followsCollection.findOne({ followedId: this.followedId, authorId: new ObjectID(this.authorId) })
  if (action == "create") {
    if (doesFollowAlreadyExist) {
      this.errors.push("You are already following this user.")
    }
  }
  if (action == "delete") {
    if (!doesFollowAlreadyExist) {
      this.errors.push("You cannot stop following someone you do not already follow.")
    }
  }

  // should not be able to follow yourself
  if (this.followedId.equals(this.authorId)) {
    this.errors.push("You cannot follow yourself.")
  }
}

Follow.prototype.create = function () {
  return new Promise(async (resolve, reject) => {
    this.cleanUp()
    await this.validate("create")
    if (!this.errors.length) {
      await followsCollection.insertOne({ followedId: this.followedId, authorId: new ObjectID(this.authorId) })
      resolve()
    } else {
      reject(this.errors)
    }
  })
}

Follow.prototype.delete = function () {
  return new Promise(async (resolve, reject) => {
    this.cleanUp()
    await this.validate("delete")
    if (!this.errors.length) {
      await followsCollection.deleteOne({ followedId: this.followedId, authorId: new ObjectID(this.authorId) })
      resolve()
    } else {
      reject(this.errors)
    }
  })
}

Follow.isVisitorFollowing = async function (followedId, visitorId) {
  let followDoc = await followsCollection.findOne({ followedId: followedId, authorId: new ObjectID(visitorId) })
  if (followDoc) {
    return true
  } else {
    return false
  }
}

Follow.getFollowersById = function (id) {
  return new Promise(async (resolve, reject) => {
    try {
      let followers = await followsCollection
        .aggregate([
          { $match: { followedId: id } },
          { $lookup: { from: "users", localField: "authorId", foreignField: "_id", as: "userDoc" } },
          {
            $project: {
              username: { $arrayElemAt: ["$userDoc.username", 0] },
              email: { $arrayElemAt: ["$userDoc.email", 0] }
            }
          }
        ])
        .toArray()
      followers = followers.map(function (follower) {
        let user = new User(follower, true)
        return { username: follower.username, avatar: user.avatar }
      })
      resolve(followers)
    } catch (e) {
      reject()
    }
  })
}

Follow.getFollowingById = function (id) {
  return new Promise(async (resolve, reject) => {
    try {
      let followers = await followsCollection
        .aggregate([
          { $match: { authorId: id } },
          { $lookup: { from: "users", localField: "followedId", foreignField: "_id", as: "userDoc" } },
          {
            $project: {
              username: { $arrayElemAt: ["$userDoc.username", 0] },
              email: { $arrayElemAt: ["$userDoc.email", 0] }
            }
          }
        ])
        .toArray()
      followers = followers.map(function (follower) {
        let user = new User(follower, true)
        return { username: follower.username, avatar: user.avatar }
      })
      resolve(followers)
    } catch (e) {
      reject()
    }
  })
}

Follow.countFollowersById = function (id) {
  return new Promise(async (resolve, reject) => {
    let followerCount = await followsCollection.countDocuments({ followedId: id })
    resolve(followerCount)
  })
}

Follow.countFollowingById = function (id) {
  return new Promise(async (resolve, reject) => {
    let count = await followsCollection.countDocuments({ authorId: id })
    resolve(count)
  })
}

module.exports = Follow


================================================
FILE: backend-api/models/Post.js
================================================
const postsCollection = require("../db").db().collection("posts")
const followsCollection = require("../db").db().collection("follows")
const ObjectId = require("mongodb").ObjectId
const User = require("./User")
const sanitizeHTML = require("sanitize-html")

postsCollection.createIndex({ title: "text", body: "text" })

let Post = function (data, userid, requestedPostId) {
  this.data = data
  this.errors = []
  this.userid = userid
  this.requestedPostId = requestedPostId
}

Post.prototype.cleanUp = function () {
  if (typeof this.data.title != "string") {
    this.data.title = ""
  }
  if (typeof this.data.body != "string") {
    this.data.body = ""
  }

  // get rid of any bogus properties
  this.data = {
    title: sanitizeHTML(this.data.title.trim(), { allowedTags: [], allowedAttributes: {} }),
    body: sanitizeHTML(this.data.body.trim(), { allowedTags: [], allowedAttributes: {} }),
    createdDate: new Date(),
    author: new ObjectId(this.userid)
  }
}

Post.prototype.validate = function () {
  if (this.data.title == "") {
    this.errors.push("You must provide a title.")
  }
  if (this.data.body == "") {
    this.errors.push("You must provide post content.")
  }
}

Post.prototype.create = function () {
  return new Promise((resolve, reject) => {
    this.cleanUp()
    this.validate()
    if (!this.errors.length) {
      // save post into database
      postsCollection
        .insertOne(this.data)
        .then(info => {
          resolve(info.insertedId)
        })
        .catch(e => {
          this.errors.push("Please try again later.")
          reject(this.errors)
        })
    } else {
      reject(this.errors)
    }
  })
}

Post.prototype.update = function () {
  return new Promise(async (resolve, reject) => {
    try {
      let post = await Post.findSingleById(this.requestedPostId, this.userid)
      if (post.isVisitorOwner) {
        // actually update the db
        let status = await this.actuallyUpdate()
        resolve(status)
      } else {
        reject()
      }
    } catch (e) {
      reject()
    }
  })
}

Post.prototype.actuallyUpdate = function () {
  return new Promise(async (resolve, reject) => {
    this.cleanUp()
    this.validate()
    if (!this.errors.length) {
      await postsCollection.findOneAndUpdate({ _id: new ObjectId(this.requestedPostId) }, { $set: { title: this.data.title, body: this.data.body } })
      resolve("success")
    } else {
      resolve("failure")
    }
  })
}

Post.reusablePostQuery = function (uniqueOperations, visitorId, finalOperations = []) {
  return new Promise(async function (resolve, reject) {
    let aggOperations = uniqueOperations
      .concat([
        { $lookup: { from: "users", localField: "author", foreignField: "_id", as: "authorDocument" } },
        {
          $project: {
            title: 1,
            body: 1,
            createdDate: 1,
            authorId: "$author",
            author: { $arrayElemAt: ["$authorDocument", 0] }
          }
        }
      ])
      .concat(finalOperations)

    let posts = await postsCollection.aggregate(aggOperations).toArray()

    // clean up author property in each post object
    posts = posts.map(function (post) {
      post.isVisitorOwner = post.authorId.equals(visitorId)
      post.authorId = undefined

      post.author = {
        username: post.author.username,
        avatar: new User(post.author, true).avatar
      }

      return post
    })

    resolve(posts)
  })
}

Post.findSingleById = function (id, visitorId) {
  return new Promise(async function (resolve, reject) {
    if (typeof id != "string" || !ObjectId.isValid(id)) {
      reject()
      return
    }

    let posts = await Post.reusablePostQuery([{ $match: { _id: new ObjectId(id) } }], visitorId)

    if (posts.length) {
      resolve(posts[0])
    } else {
      reject()
    }
  })
}

Post.findByAuthorId = function (authorId) {
  return Post.reusablePostQuery([{ $match: { author: authorId } }, { $sort: { createdDate: -1 } }])
}

Post.delete = function (postIdToDelete, currentUserId) {
  return new Promise(async (resolve, reject) => {
    try {
      let post = await Post.findSingleById(postIdToDelete, currentUserId)
      if (post.isVisitorOwner) {
        await postsCollection.deleteOne({ _id: new ObjectId(postIdToDelete) })
        resolve()
      } else {
        reject()
      }
    } catch (e) {
      reject()
    }
  })
}

Post.search = function (searchTerm) {
  return new Promise(async (resolve, reject) => {
    if (typeof searchTerm == "string") {
      let posts = await Post.reusablePostQuery([{ $match: { $text: { $search: searchTerm } } }], undefined, [{ $sort: { score: { $meta: "textScore" } } }])
      resolve(posts)
    } else {
      reject()
    }
  })
}

Post.countPostsByAuthor = function (id) {
  return new Promise(async (resolve, reject) => {
    let postCount = await postsCollection.countDocuments({ author: id })
    resolve(postCount)
  })
}

Post.getFeed = async function (id) {
  // create an array of the user ids that the current user follows
  let followedUsers = await followsCollection.find({ authorId: new ObjectId(id) }).toArray()
  followedUsers = followedUsers.map(function (followDoc) {
    return followDoc.followedId
  })

  // look for posts where the author is in the above array of followed users
  return Post.reusablePostQuery([{ $match: { author: { $in: followedUsers } } }, { $sort: { createdDate: -1 } }])
}

module.exports = Post


================================================
FILE: backend-api/models/User.js
================================================
const bcrypt = require("bcryptjs")
const usersCollection = require('../db').db().collection("users")
const validator = require("validator")
const md5 = require('md5')

let User = function(data, getAvatar) {
  this.data = data
  this.errors = []
  if (getAvatar == undefined) {getAvatar = false}
  if (getAvatar) {this.getAvatar()}
}

User.prototype.cleanUp = function() {
  if (typeof(this.data.username) != "string") {this.data.username = ""}
  if (typeof(this.data.email) != "string") {this.data.email = ""}
  if (typeof(this.data.password) != "string") {this.data.password = ""}

  // get rid of any bogus properties
  this.data = {
    username: this.data.username.trim().toLowerCase(),
    email: this.data.email.trim().toLowerCase(),
    password: this.data.password
  }
}

User.prototype.validate = function() {
  return new Promise(async (resolve, reject) => {
    if (this.data.username == "") {this.errors.push("You must provide a username.")}
    if (this.data.username != "" && !validator.isAlphanumeric(this.data.username)) {this.errors.push("Username can only contain letters and numbers.")}
    if (!validator.isEmail(this.data.email)) {this.errors.push("You must provide a valid email address.")}
    if (this.data.password == "") {this.errors.push("You must provide a password.")}
    if (this.data.password.length > 0 && this.data.password.length < 12) {this.errors.push("Password must be at least 12 characters.")}
    if (this.data.password.length > 50) {this.errors.push("Password cannot exceed 50 characters.")}
    if (this.data.username.length > 0 && this.data.username.length < 3) {this.errors.push("Username must be at least 3 characters.")}
    if (this.data.username.length > 30) {this.errors.push("Username cannot exceed 30 characters.")}
  
    // Only if username is valid then check to see if it's already taken
    if (this.data.username.length > 2 && this.data.username.length < 31 && validator.isAlphanumeric(this.data.username)) {
      let usernameExists = await usersCollection.findOne({username: this.data.username})
      if (usernameExists) {this.errors.push("That username is already taken.")}
    }
  
    // Only if email is valid then check to see if it's already taken
    if (validator.isEmail(this.data.email)) {
      let emailExists = await usersCollection.findOne({email: this.data.email})
      if (emailExists) {this.errors.push("That email is already being used.")}
    }
    resolve()
  })
}

User.prototype.login = function() {
  return new Promise((resolve, reject) => {
    this.cleanUp()
    usersCollection.findOne({username: this.data.username}).then((attemptedUser) => {
      if (attemptedUser && bcrypt.compareSync(this.data.password, attemptedUser.password)) {
        this.data = attemptedUser
        this.getAvatar()
        resolve("Congrats!")
      } else {
        reject("Invalid username / password.")
      }
    }).catch(function(e) {
      reject("Please try again later.")
    })
  })
}

User.prototype.register = function() {
  return new Promise(async (resolve, reject) => {
    // Step #1: Validate user data
    this.cleanUp()
    await this.validate()
  
    // Step #2: Only if there are no validation errors 
    // then save the user data into a database
    if (!this.errors.length) {
      // hash user password
      let salt = bcrypt.genSaltSync(10)
      this.data.password = bcrypt.hashSync(this.data.password, salt)
      await usersCollection.insertOne(this.data)
      this.getAvatar()
      resolve()
    } else {
      reject(this.errors)
    }
  })
}

User.prototype.getAvatar = function() {
  this.avatar = `https://gravatar.com/avatar/${md5(this.data.email)}?s=128`
}

User.findByUsername = function(username) {
  return new Promise(function(resolve, reject) {
    if (typeof(username) != "string") {
      reject()
      return
    }
    usersCollection.findOne({username: username}).then(function(userDoc) {
      if (userDoc) {
        userDoc = new User(userDoc, true)
        userDoc = {
          _id: userDoc.data._id,
          username: userDoc.data.username,
          avatar: userDoc.avatar
        }
        resolve(userDoc)
      } else {
        reject()
      }
    }).catch(function(e) {
      reject()
    })
  })
}

User.doesEmailExist = function(email) {
  return new Promise(async function(resolve, reject) {
    if (typeof(email) != "string") {
      resolve(false)
      return
    }

    let user = await usersCollection.findOne({email: email})
    if (user) {
      resolve(true)
    } else {
      resolve(false)
    }
  })
}

module.exports = User

================================================
FILE: backend-api/package.json
================================================
{
  "name": "complex-app",
  "version": "1.0.0",
  "description": "hey dude",
  "main": "db.js",
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/mongodb/node-mongodb-native.git"
  },
  "scripts": {
    "watch": "nodemon db",
    "start": "node db",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "overrides": {
    "semver": "7.5.3"
  },
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "jsonwebtoken": "^9.0.0",
    "md5": "^2.3.0",
    "mongodb": "^5.6.0",
    "nodemon": "^2.0.22",
    "sanitize-html": "^2.11.0",
    "socket.io": "^4.7.1",
    "validator": "^13.9.0"
  }
}


================================================
FILE: backend-api/router.js
================================================
const apiRouter = require("express").Router()
const userController = require("./controllers/userController")
const postController = require("./controllers/postController")
const followController = require("./controllers/followController")
const cors = require("cors")

apiRouter.use(cors())

apiRouter.get("/", (req, res) => res.json("Hello, if you see this message that means your backend is up and running successfully. Congrats! Now let's continue learning React!"))

// check token to log out front-end if expired
apiRouter.post("/checkToken", userController.checkToken)

apiRouter.post("/getHomeFeed", userController.apiMustBeLoggedIn, userController.apiGetHomeFeed)
apiRouter.post("/register", userController.apiRegister)
apiRouter.post("/login", userController.apiLogin)
apiRouter.get("/post/:id", postController.reactApiViewSingle)
apiRouter.post("/post/:id/edit", userController.apiMustBeLoggedIn, postController.apiUpdate)
apiRouter.delete("/post/:id", userController.apiMustBeLoggedIn, postController.apiDelete)
apiRouter.post("/create-post", userController.apiMustBeLoggedIn, postController.apiCreate)
apiRouter.post("/search", postController.search)

apiRouter.post("/doesUsernameExist", userController.doesUsernameExist)
apiRouter.post("/doesEmailExist", userController.doesEmailExist)

// profile related routes
apiRouter.post("/profile/:username", userController.ifUserExists, userController.sharedProfileData, userController.profileBasicData)
apiRouter.get("/profile/:username/posts", userController.ifUserExists, userController.apiGetPostsByUsername)
apiRouter.get("/profile/:username/followers", userController.ifUserExists, userController.profileFollowers)
apiRouter.get("/profile/:username/following", userController.ifUserExists, userController.profileFollowing)

// follow routes
apiRouter.post("/addFollow/:username", userController.apiMustBeLoggedIn, followController.apiAddFollow)
apiRouter.post("/removeFollow/:username", userController.apiMustBeLoggedIn, followController.apiRemoveFollow)

module.exports = apiRouter


================================================
FILE: generateHtml.js
================================================
import React from "react"
import ReactDOMServer from "react-dom/server"
import fs from "fs"
import Footer from "./app/components/Footer"
import Header from "./app/components/Header"
import LoadingDotsIcon from "./app/components/LoadingDotsIcon"
import { StaticRouter as Router } from "react-router"
import StateContext from "./app/StateContext"

function Shell() {
  return (
    <StateContext.Provider value={{ loggedIn: false }}>
      <Router>
        <Header staticEmpty={true} />
        <div className="py-5 my-5 text-center">
          <LoadingDotsIcon />
        </div>
        <Footer />
      </Router>
    </StateContext.Provider>
  )
}

const startOfHTML = `<!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta http-equiv="X-UA-Compatible" content="ie=edge" />
      <title>OurApp</title>
      <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
      <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" />
      <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
  
      <link rel="stylesheet" href="/main.css" />
    </head>
    <body>
      <div id="app">`

const endOfHTML = `</div>
    </body>
  </html>`

/* Use Node tools (outside the scope of this course) to setup a
  stream we can write to that saves to a file on our hard drive
*/
const fileName = "./app/index-template.html"
const writeStream = fs.createWriteStream(fileName)

// Add the start of our HTML template to the stream
writeStream.write(startOfHTML)

/*
  Add the actual React generated HTML to the stream.
  We can use ReactDomServer (you can see how we imported
  that at the very top of this file) to generate a string
  of HTML text that a Node stream can leverage.
*/
const myStream = ReactDOMServer.renderToPipeableStream(<Shell />, {
  onAllReady() {
    myStream.pipe(writeStream)
    // End the stream with the final bit of our HTML
    writeStream.end(endOfHTML)
  }
})


================================================
FILE: html-templates/about.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <form class="mb-0 pt-2 pt-md-0">
          <div class="row align-items-center">
            <div class="col-md mr-0 pr-md-0 mb-3 mb-md-0">
              <input name="username" class="form-control form-control-sm input-dark" type="text" placeholder="Username" autocomplete="off" />
            </div>
            <div class="col-md mr-0 pr-md-0 mb-3 mb-md-0">
              <input name="password" class="form-control form-control-sm input-dark" type="password" placeholder="Password" />
            </div>
            <div class="col-md-auto">
              <button class="btn btn-success btn-sm">Sign In</button>
            </div>
          </div>
        </form>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <h2>About Us</h2>
      <p class="lead text-muted">Lorem ipsum dolor sit amet consectetur adipisicing elit. Corporis dolorum labore quisquam vel id dicta fuga! Ducimus, quo. Dolore commodi aliquid error veritatis consequuntur, excepturi cumque fuga eum incidunt doloremque?</p>
      <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. At qui enim rem totam voluptatum. Aut saepe temporibus, facilis ex a iste expedita minima dolorum dicta doloribus libero aliquid, quae maxime? Lorem ipsum dolor sit amet consectetur adipisicing elit. Fugiat suscipit beatae eum, est soluta ducimus ratione et impedit sapiente, nihil, atque dignissimos adipisci? Totam atque officia quis voluptates sed veniam?</p>
      <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita voluptates quisquam possimus tenetur, dicta enim rerum quis, quaerat id nobis provident quo dolorum sapiente temporibus facere non repellendus consequatur cupiditate!</p>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/chat-is-visible.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <div class="flex-row my-3 my-md-0">
          <a href="#" class="text-white mr-2 header-search-icon">
            <i class="fas fa-search"></i>
          </a>
          <span class="mr-2 header-chat-icon text-danger">
            <i class="fas fa-comment"></i>
            <span class="chat-count-badge text-white">3</span>
          </span>
          <a href="#" class="mr-2">
            <img class="small-header-avatar" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
          </a>
          <a class="btn btn-sm btn-success mr-2" href="/create-post">
            Create Post
          </a>
          <button class="btn btn-sm btn-secondary">
            Sign Out
          </button>
        </div>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <h2>
        <img class="avatar-small" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> brad
        <button class="btn btn-primary btn-sm ml-2">Follow <i class="fas fa-user-plus"></i></button>
      </h2>

      <div class="profile-nav nav nav-tabs pt-2 mb-4">
        <a href="#" class="active nav-item nav-link">
          Posts: 3
        </a>
        <a href="#" class="nav-item nav-link">
          Followers: 101
        </a>
        <a href="#" class="nav-item nav-link">
          Following: 40
        </a>
      </div>

      <div class="list-group">
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #1</strong>
          <span class="text-muted small">on 2/10/2020 </span>
        </a>
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #2</strong>
          <span class="text-muted small">on 2/10/2020 </span>
        </a>
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #3</strong>
          <span class="text-muted small">on 2/10/2020 </span>
        </a>
      </div>
    </div>

    <div id="chat-wrapper" class="chat-wrapper chat-wrapper--is-visible shadow border-top border-left border-right">
      <div class="chat-title-bar bg-primary">
        Chat
        <span class="chat-title-bar-close">
          <i class="fas fa-times-circle"></i>
        </span>
      </div>
      <div id="chat" class="chat-log">
        <div class="chat-self">
          <div class="chat-message">
            <div class="chat-message-inner">Hey, how are you?</div>
          </div>
          <img class="chat-avatar avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
        </div>

        <div class="chat-other">
          <a href="#">
            <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" />
          </a>
          <div class="chat-message">
            <div class="chat-message-inner">
              <a href="#">
                <strong>barksalot:</strong>
              </a>
              Hey, I am good, how about you?
            </div>
          </div>
        </div>
      </div>
      <form id="chatForm" class="chat-form border-top">
        <input type="text" class="chat-field" id="chatField" placeholder="Type a message…" autocomplete="off" />
      </form>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/create-post.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <div class="flex-row my-3 my-md-0">
          <a href="#" class="text-white mr-2 header-search-icon">
            <i class="fas fa-search"></i>
          </a>
          <span class="mr-2 header-chat-icon text-danger">
            <i class="fas fa-comment"></i>
            <span class="chat-count-badge text-white">3</span>
          </span>
          <a href="#" class="mr-2">
            <img class="small-header-avatar" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
          </a>
          <a class="btn btn-sm btn-success mr-2" href="/create-post">
            Create Post
          </a>
          <button class="btn btn-sm btn-secondary">
            Sign Out
          </button>
        </div>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <form>
        <div class="form-group">
          <label for="post-title" class="text-muted mb-1">
            <small>Title</small>
          </label>
          <input autofocus name="title" id="post-title" class="form-control form-control-lg form-control-title" type="text" placeholder="" autocomplete="off" />
        </div>

        <div class="form-group">
          <label for="post-body" class="text-muted mb-1 d-block">
            <small>Body Content</small>
          </label>
          <textarea name="body" id="post-body" class="body-content tall-textarea form-control" type="text"></textarea>
        </div>

        <button class="btn btn-primary">Save New Post</button>
      </form>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/index-empty-feed.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <div class="flex-row my-3 my-md-0">
          <a href="#" class="text-white mr-2 header-search-icon">
            <i class="fas fa-search"></i>
          </a>
          <span class="mr-2 header-chat-icon text-white">
            <i class="fas fa-comment"></i>
            <span class="chat-count-badge text-white"> </span>
          </span>
          <a href="#" class="mr-2">
            <img class="small-header-avatar" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
          </a>
          <a class="btn btn-sm btn-success mr-2" href="/create-post">
            Create Post
          </a>
          <button class="btn btn-sm btn-secondary">
            Sign Out
          </button>
        </div>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <h2 class="text-center">Hello <strong>brad</strong>, your feed is empty.</h2>
      <p class="lead text-muted text-center">Your feed displays the latest posts from the people you follow. If you don&rsquo;t have any friends to follow that&rsquo;s okay; you can use the &ldquo;Search&rdquo; feature in the top menu bar to find content written by people with similar interests and then follow them.</p>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/index-feed.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <div class="flex-row my-3 my-md-0">
          <a href="#" class="text-white mr-2 header-search-icon">
            <i class="fas fa-search"></i>
          </a>
          <span class="mr-2 header-chat-icon text-white">
            <i class="fas fa-comment"></i>
            <span class="chat-count-badge text-white"> </span>
          </span>
          <a href="#" class="mr-2">
            <img class="small-header-avatar" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
          </a>
          <a class="btn btn-sm btn-success mr-2" href="/create-post">
            Create Post
          </a>
          <button class="btn btn-sm btn-secondary">
            Sign Out
          </button>
        </div>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <h2 class="text-center mb-4">The Latest From Those You Follow</h2>
      <div class="list-group">
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #1</strong>
          <span class="text-muted small">by brad on 2/10/2020 </span>
        </a>
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> <strong>Example Post #2</strong>
          <span class="text-muted small">by barksalot on 2/10/2020 </span>
        </a>
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #3</strong>
          <span class="text-muted small">by brad on 2/10/2020 </span>
        </a>
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> <strong>Example Post #4</strong>
          <span class="text-muted small">by barksalot on 2/10/2020 </span>
        </a>
      </div>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/index-guest.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <form class="mb-0 pt-2 pt-md-0">
          <div class="row align-items-center">
            <div class="col-md mr-0 pr-md-0 mb-3 mb-md-0">
              <input name="username" class="form-control form-control-sm input-dark" type="text" placeholder="Username" autocomplete="off" />
            </div>
            <div class="col-md mr-0 pr-md-0 mb-3 mb-md-0">
              <input name="password" class="form-control form-control-sm input-dark" type="password" placeholder="Password" />
            </div>
            <div class="col-md-auto">
              <button class="btn btn-success btn-sm">Sign In</button>
            </div>
          </div>
        </form>
      </div>
    </header>

    <div class="container py-md-5">
      <div class="row align-items-center">
        <div class="col-lg-7 py-3 py-md-5">
          <h1 class="display-3">Remember Writing?</h1>
          <p class="lead text-muted">Are you sick of short tweets and impersonal &ldquo;shared&rdquo; posts that are reminiscent of the late 90&rsquo;s email forwards? We believe getting back to actually writing is the key to enjoying the internet again.</p>
        </div>
        <div class="col-lg-5 pl-lg-5 pb-3 py-lg-5">
          <form>
            <div class="form-group">
              <label for="username-register" class="text-muted mb-1">
                <small>Username</small>
              </label>
              <input id="username-register" name="username" class="form-control" type="text" placeholder="Pick a username" autocomplete="off" />
            </div>
            <div class="form-group">
              <label for="email-register" class="text-muted mb-1">
                <small>Email</small>
              </label>
              <input id="email-register" name="email" class="form-control" type="text" placeholder="you@example.com" autocomplete="off" />
            </div>
            <div class="form-group">
              <label for="password-register" class="text-muted mb-1">
                <small>Password</small>
              </label>
              <input id="password-register" name="password" class="form-control" type="password" placeholder="Create a password" />
            </div>
            <button type="submit" class="py-3 mt-4 btn btn-lg btn-success btn-block">
              Sign up for ComplexApp
            </button>
          </form>
        </div>
      </div>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/main.css
================================================
body {
  font-family: "Public Sans", sans-serif;
}

.header-bar {
  background-color: #292929;
}

.container--narrow {
  max-width: 732px;
}

.header-search-icon {
  position: relative;
  top: 3px;
}

.header-chat-icon {
  cursor: pointer;
  position: relative;
  top: 3px;
}

.chat-count-badge {
  text-align: center;
  position: absolute;
  top: 2px;
  left: 0px;
  width: 16px;
  font-size: 0.6rem;
  font-weight: bold;
}

.avatar-small {
  width: 32px;
  height: 32px;
  border-radius: 16px;
  margin-right: 5px;
  position: relative;
  top: -3px;
}

.avatar-tiny {
  width: 24px;
  height: 24px;
  border-radius: 12px;
  margin-right: 4px;
  position: relative;
  top: -1px;
}

.form-control-title {
  font-size: 2rem;
  font-weight: 500;
}

.body-content {
  font-size: 1.2rem;
  line-height: 1.75;
  color: #292929;
}

.body-content p,
.body-content ul,
.body-content ol {
  margin-bottom: 1.75rem;
}

.input-dark {
  background-color: #444;
  border-color: transparent;
  color: #ffffff;
}

.input-dark:focus {
  color: #ffffff;
  background-color: #555;
  border-color: #80bdff;
  outline: 0;
  box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}

.input-dark::-webkit-input-placeholder {
  color: #888;
}
.input-dark::-moz-placeholder {
  color: #888;
}
.input-dark:-ms-input-placeholder {
  color: #888;
}
.input-dark:-moz-placeholder {
  color: #888;
}

@media (min-width: 768px) {
  .input-dark {
    width: auto;
  }
}

.display-3 {
  font-size: 4.2rem;
}

@media (max-width: 1199px) {
  .display-3 {
    font-size: 3.4rem;
  }
}

@media (max-width: 768px) {
  .display-3 {
    font-size: 2.5rem;
  }
}

.form-group {
  position: relative;
}

.liveValidateMessage {
  top: -6px;
  position: absolute;
  z-index: 1;
  padding-top: 6px;
  padding-bottom: 16px;
  padding-left: 0.8rem;
  padding-right: 0.8rem;
}

.liveValidateMessage--visible {
  opacity: 1;
  transform: translateY(0);
}

.liveValidateMessage-enter {
  opacity: 0;
  transform: translateY(100%);
}

.liveValidateMessage-enter-active {
  opacity: 1;
  transform: translateY(0);
  transition: 0.33s opacity ease-in-out, 0.33s transform ease-in-out;
}

.liveValidateMessage-exit {
  opacity: 1;
  transform: translateY(0);
}

.liveValidateMessage-exit-active {
  opacity: 0;
  transform: translateY(100%);
  transition: 0.33s opacity ease-in-out, 0.33s transform ease-in-out;
}

.form-group input,
.form-group textarea {
  position: relative;
  z-index: 2;
}

textarea.tall-textarea {
  height: 160px;
}

@media (min-width: 768px) {
  textarea.tall-textarea {
    height: 320px;
  }
}

.delete-post-button {
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
}

/* Search Overaly */
.search-overlay {
  display: flex;
  flex-direction: column;
  position: fixed;
  z-index: 9000;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background-color: rgba(215, 215, 215, 0.911);
}

.search-overlay-enter {
  opacity: 0;
  transform: scale(1.3);
}

.search-overlay-enter-active {
  opacity: 1;
  transform: scale(1);
  transition: 0.33s visibility ease-in-out, 0.33s opacity ease-in-out, 0.33s transform ease-in-out;
}

.search-overlay-exit {
  opacity: 1;
  transform: scale(1);
}

.search-overlay-exit-active {
  opacity: 0;
  transform: scale(1.3);
  transition: 0.33s visibility ease-in-out, 0.33s opacity ease-in-out, 0.33s transform ease-in-out;
}

.search-overlay-icon {
  color: #007bff;
  font-size: 1.4rem;
  margin: 0;
  margin-right: 10px;
}

.live-search-field {
  background-color: transparent;
  border: none;
  font-size: 1.1rem;
  outline: none;
  flex: 1;
  color: #007bff;
}

.live-search-results {
  opacity: 0;
  transition: all 0.3s ease-out;
  transform: scale(1.07);
}

.live-search-results--visible {
  opacity: 1;
  transform: scale(1);
}

.search-overlay-top {
  background-color: #fff;
  /* background-color: rgba(0, 0, 0, .79); */
}

.search-overlay-top .container {
  position: relative;
  display: flex;
  align-items: center;
  padding-top: 15px;
  padding-bottom: 15px;
}

.search-overlay-bottom {
  overflow: auto;
}

.close-live-search {
  font-size: 1.5rem;
  cursor: pointer;
  opacity: 0.75;
  line-height: 1;
  color: #292929;
}

@media (min-width: 700px) {
  .live-search-field {
    font-size: 2.5rem;
  }

  .close-live-search {
    font-size: 3rem;
  }

  .search-overlay-icon {
    font-size: 3rem;
  }
}

.close-live-search:hover {
  opacity: 1;
}

@-webkit-keyframes spin {
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@keyframes spin {
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

.circle-loader {
  opacity: 0;
  transition: opacity 0.45s ease-out, visibility 0.45s ease-out;
  visibility: hidden;
  position: absolute;
  left: 50%;
  box-sizing: border-box;
  width: 65px;
  height: 65px;
  border-radius: 100%;
  border: 10px solid rgba(73, 80, 87, 0.2);
  border-top-color: #495057;
  will-change: -webkit-transform, transform;
  -webkit-transform: rotate(0deg);
  transform: rotate(0deg);
  -webkit-animation: spin 1s infinite linear;
  animation: spin 1s infinite linear;
}

.circle-loader--visible {
  visibility: visible;
  opacity: 1;
}

/* End Search Overlay */

/* Chat */
.chat-wrapper {
  position: fixed;
  z-index: 5;
  bottom: 0;
  right: 20px;
  width: 290px;
  height: 350px;
  background-color: #fff;
  display: flex;
  flex-direction: column;
  opacity: 0;
  transform: translateY(100%);
  transition: 0.33s opacity ease-in-out, 0.33s transform ease-in-out;
}

.chat-wrapper--is-visible {
  opacity: 1;
  transform: translateY(0);
}

.chat-title-bar {
  background-color: #292929;
  color: #fff;
  padding: 4px 7px;
  display: flex;
  justify-content: space-between;
}

.chat-title-bar-close {
  opacity: 0.7;
  cursor: pointer;
}

.chat-title-bar-close:hover {
  opacity: 1;
}

.chat-log {
  padding: 8px;
  flex: 1;
  overflow: auto;
}

.chat-self,
.chat-other {
  font-size: 0.75rem;
  display: flex;
  align-items: center;
  margin-bottom: 7px;
}

.chat-self {
  padding-left: 25%;
}

.chat-self .chat-avatar {
  margin-left: 6px;
}

.chat-self .chat-message {
  flex: 1;
  display: flex;
  justify-content: flex-end;
}

.chat-self .chat-message-inner {
  text-align: right;
  padding: 4px 7px;
  border-radius: 12px;
  background-color: #007bff;
  color: #fff;
}

.chat-other {
  padding-right: 25%;
}

.chat-other .chat-avatar {
  margin-right: 6px;
}

.chat-other .chat-message {
  flex: 1;
  display: flex;
  justify-content: flex-start;
}

.chat-other .chat-message-inner {
  padding: 4px 7px;
  border-radius: 12px;
  background-color: #f1f0f0;
}

.chat-message a {
  color: #212529;
}

.chat-field {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 7px;
  border: none;
  outline: none;
  font-size: 0.75rem;
}

.floating-alert {
  display: none;
  position: absolute;
  z-index: 999;
  top: 38px;
  left: 50%;
  transform: translateX(-50%);
  -moz-animation: floatingAlert ease-in 5s forwards;
  -webkit-animation: floatingAlert ease-in 5s forwards;
  -o-animation: floatingAlert ease-in 5s forwards;
  animation: floatingAlert ease-in 5s forwards;
  -webkit-animation-fill-mode: forwards;
  animation-fill-mode: forwards;
}

.floating-alerts .floating-alert:last-of-type {
  display: block;
}

@keyframes floatingAlert {
  0% {
    opacity: 0;
    visibility: hidden;
    transform: translateX(-50%) scale(1.2);
  }

  9% {
    opacity: 1;
    visibility: visible;
    transform: translateX(-50%) scale(1);
  }

  91% {
    opacity: 1;
    visibility: visible;
    transform: translateX(-50%) scale(1);
  }

  100% {
    opacity: 0;
    visibility: hidden;
    transform: translateX(-50%) scale(1.2);
  }
}

@-webkit-keyframes floatingAlert {
  0% {
    opacity: 0;
    visibility: hidden;
    transform: translateX(-50%) scale(1.2);
  }

  9% {
    opacity: 1;
    visibility: visible;
    transform: translateX(-50%) scale(1);
  }

  91% {
    opacity: 1;
    visibility: visible;
    transform: translateX(-50%) scale(1);
  }

  100% {
    opacity: 0;
    visibility: hidden;
    transform: translateX(-50%) scale(1.2);
  }
}

.small-header-avatar {
  width: 32px;
  height: 32px;
  border-radius: 16px;
}

.custom-tooltip {
  padding: 8px 10px !important;
}

/* Dots Loading Animation */
.dots-loading {
  margin: 0 auto;
  text-align: center;
}

.dots-loading::before,
.dots-loading::after {
  content: " ";
}

.dots-loading div,
.dots-loading::before,
.dots-loading::after {
  margin: 35px 5px;
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background-color: #c4c4c4;
  opacity: 0;
}

.dots-loading::before {
  -moz-animation: opacitychange 1s ease-in-out infinite;
  -webkit-animation: opacitychange 1s ease-in-out infinite;
  -o-animation: opacitychange 1s ease-in-out infinite;
  animation: opacitychange 1s ease-in-out infinite;
}

.dots-loading div {
  -moz-animation: opacitychange 1s ease-in-out 0.33s infinite;
  -webkit-animation: opacitychange 1s ease-in-out 0.33s infinite;
  -o-animation: opacitychange 1s ease-in-out 0.33s infinite;
  animation: opacitychange 1s ease-in-out 0.33s infinite;
  -webkit-animation-fill-mode: infinite;
  animation-fill-mode: infinite;
}

.dots-loading::after {
  -moz-animation: opacitychange 1s ease-in-out 0.66s infinite;
  -webkit-animation: opacitychange 1s ease-in-out 0.66s infinite;
  -o-animation: opacitychange 1s ease-in-out 0.66s infinite;
  animation: opacitychange 1s ease-in-out 0.66s infinite;
  -webkit-animation-fill-mode: infinite;
  animation-fill-mode: infinite;
}

@keyframes opacitychange {
  0%,
  100% {
    opacity: 0;
  }

  60% {
    opacity: 1;
  }
}

@-webkit-keyframes opacitychange {
  0%,
  100% {
    opacity: 0;
  }

  60% {
    opacity: 1;
  }
}


================================================
FILE: html-templates/profile-followers.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <div class="flex-row my-3 my-md-0">
          <a href="#" class="text-white mr-2 header-search-icon">
            <i class="fas fa-search"></i>
          </a>
          <span class="mr-2 header-chat-icon text-white">
            <i class="fas fa-comment"></i>
            <span class="chat-count-badge text-white"> </span>
          </span>
          <a href="#" class="mr-2">
            <img class="small-header-avatar" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
          </a>
          <a class="btn btn-sm btn-success mr-2" href="/create-post">
            Create Post
          </a>
          <button class="btn btn-sm btn-secondary">
            Sign Out
          </button>
        </div>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <h2>
        <img class="avatar-small" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> brad
        <button class="btn btn-primary btn-sm ml-2">Follow <i class="fas fa-user-plus"></i></button>
      </h2>

      <div class="profile-nav nav nav-tabs pt-2 mb-4">
        <a href="#" class="nav-item nav-link">
          Posts: 3
        </a>
        <a href="#" class="active nav-item nav-link">
          Followers: 101
        </a>
        <a href="#" class="nav-item nav-link">
          Following: 40
        </a>
      </div>

      <div class="list-group">
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
      </div>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/profile-posts.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <div class="flex-row my-3 my-md-0">
          <a href="#" class="text-white mr-2 header-search-icon">
            <i class="fas fa-search"></i>
          </a>
          <span class="mr-2 header-chat-icon text-white">
            <i class="fas fa-comment"></i>
            <span class="chat-count-badge text-white"> </span>
          </span>
          <a href="#" class="mr-2">
            <img class="small-header-avatar" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
          </a>
          <a class="btn btn-sm btn-success mr-2" href="/create-post">
            Create Post
          </a>
          <button class="btn btn-sm btn-secondary">
            Sign Out
          </button>
        </div>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <h2>
        <img class="avatar-small" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> brad
        <button class="btn btn-primary btn-sm ml-2">Follow <i class="fas fa-user-plus"></i></button>
      </h2>

      <div class="profile-nav nav nav-tabs pt-2 mb-4">
        <a href="#" class="active nav-item nav-link">
          Posts: 3
        </a>
        <a href="#" class="nav-item nav-link">
          Followers: 101
        </a>
        <a href="#" class="nav-item nav-link">
          Following: 40
        </a>
      </div>

      <div class="list-group">
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #1</strong>
          <span class="text-muted small">on 2/10/2020 </span>
        </a>
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #2</strong>
          <span class="text-muted small">on 2/10/2020 </span>
        </a>
        <a href="#" class="list-group-item list-group-item-action">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #3</strong>
          <span class="text-muted small">on 2/10/2020 </span>
        </a>
      </div>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/search-is-visible.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <div class="flex-row my-3 my-md-0">
          <a href="#" class="text-white mr-2 header-search-icon">
            <i class="fas fa-search"></i>
          </a>
          <span class="mr-2 header-chat-icon text-white">
            <i class="fas fa-comment"></i>
            <span class="chat-count-badge text-white"> </span>
          </span>
          <a href="#" class="mr-2">
            <img class="small-header-avatar" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
          </a>
          <a class="btn btn-sm btn-success mr-2" href="/create-post">
            Create Post
          </a>
          <button class="btn btn-sm btn-secondary">
            Sign Out
          </button>
        </div>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <h2>
        <img class="avatar-small" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> brad
        <button class="btn btn-primary btn-sm ml-2">Follow <i class="fas fa-user-plus"></i></button>
      </h2>

      <div class="profile-nav nav nav-tabs pt-2 mb-4">
        <a href="#" class="nav-item nav-link">
          Posts: 3
        </a>
        <a href="#" class="active nav-item nav-link">
          Followers: 101
        </a>
        <a href="#" class="nav-item nav-link">
          Following: 40
        </a>
      </div>

      <div class="list-group">
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
        <a href="#" class="list-group-item list-group-item-action"> <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> barksalot </a>
      </div>
    </div>

    <div class="search-overlay">
      <div class="search-overlay-top shadow-sm">
        <div class="container container--narrow">
          <label for="live-search-field" class="search-overlay-icon">
            <i class="fas fa-search"></i>
          </label>
          <input autofocus type="text" autocomplete="off" id="live-search-field" class="live-search-field" placeholder="What are you interested in?" />
          <span class="close-live-search">
            <i class="fas fa-times-circle"></i>
          </span>
        </div>
      </div>

      <div class="search-overlay-bottom">
        <div class="container container--narrow py-3">
          <div class="live-search-results live-search-results--visible">
            <div class="list-group shadow-sm">
              <div class="list-group-item active"><strong>Search Results</strong> (3 items found)</div>
              <a href="#" class="list-group-item list-group-item-action">
                <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #1</strong>
                <span class="text-muted small">by brad on 2/10/2020 </span>
              </a>
              <a href="#" class="list-group-item list-group-item-action">
                <img class="avatar-tiny" src="https://gravatar.com/avatar/b9216295c1e3931655bae6574ac0e4c2?s=128" /> <strong>Example Post #2</strong>
                <span class="text-muted small">by barksalot on 2/10/2020 </span>
              </a>
              <a href="#" class="list-group-item list-group-item-action">
                <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" /> <strong>Example Post #3</strong>
                <span class="text-muted small">by brad on 2/10/2020 </span>
              </a>
            </div>
          </div>
        </div>
      </div>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/single-post.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <div class="flex-row my-3 my-md-0">
          <a href="#" class="text-white mr-2 header-search-icon">
            <i class="fas fa-search"></i>
          </a>
          <span class="mr-2 header-chat-icon text-white">
            <i class="fas fa-comment"></i>
            <span class="chat-count-badge text-white"> </span>
          </span>
          <a href="#" class="mr-2">
            <img class="small-header-avatar" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
          </a>
          <a class="btn btn-sm btn-success mr-2" href="/create-post">
            Create Post
          </a>
          <button class="btn btn-sm btn-secondary">
            Sign Out
          </button>
        </div>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <div class="d-flex justify-content-between">
        <h2>Example Post Title</h2>
        <span class="pt-2">
          <a href="#" class="text-primary mr-2" title="Edit"><i class="fas fa-edit"></i></a>
          <a class="delete-post-button text-danger" title="Delete"><i class="fas fa-trash"></i></a>
        </span>
      </div>

      <p class="text-muted small mb-4">
        <a href="#">
          <img class="avatar-tiny" src="https://gravatar.com/avatar/b9408a09298632b5151200f3449434ef?s=128" />
        </a>
        Posted by <a href="#">brad</a> on 2/10/2020
      </p>

      <div class="body-content">
        <p>Lorem ipsum dolor sit <strong>example</strong> post adipisicing elit. Iure ea at esse, tempore qui possimus soluta impedit natus voluptate, sapiente saepe modi est pariatur. Aut voluptatibus aspernatur fugiat asperiores at.</p>
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Beatae quod asperiores corrupti omnis qui, placeat neque modi, dignissimos, ab exercitationem eligendi culpa explicabo nulla tempora rem? Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure ea at esse, tempore qui possimus soluta impedit natus voluptate, sapiente saepe modi est pariatur. Aut voluptatibus aspernatur fugiat asperiores at.</p>
      </div>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: html-templates/terms.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>OurApp</title>
    <link href="https://fonts.googleapis.com/css?family=Public+Sans:300,400,400i,700,700i&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
    <script defer src="https://use.fontawesome.com/releases/v5.5.0/js/all.js" integrity="sha384-GqVMZRt5Gn7tB9D9q7ONtcp4gtHIUEW/yG7h98J7IpE3kpi+srfFyyB/04OV6pG0" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <header class="header-bar bg-primary mb-3">
      <div class="container d-flex flex-column flex-md-row align-items-center p-3">
        <h4 class="my-0 mr-md-auto font-weight-normal">
          <a href="/" class="text-white">
            ComplexApp
          </a>
        </h4>
        <form class="mb-0 pt-2 pt-md-0">
          <div class="row align-items-center">
            <div class="col-md mr-0 pr-md-0 mb-3 mb-md-0">
              <input name="username" class="form-control form-control-sm input-dark" type="text" placeholder="Username" autocomplete="off" />
            </div>
            <div class="col-md mr-0 pr-md-0 mb-3 mb-md-0">
              <input name="password" class="form-control form-control-sm input-dark" type="password" placeholder="Password" />
            </div>
            <div class="col-md-auto">
              <button class="btn btn-success btn-sm">Sign In</button>
            </div>
          </div>
        </form>
      </div>
    </header>

    <div class="container container--narrow py-md-5">
      <h2>Our Terms &amp; Conditions</h2>
      <p class="lead text-muted">Lorem ipsum dolor sit amet consectetur adipisicing elit. Corporis dolorum labore quisquam vel id dicta fuga! Ducimus, quo. Dolore commodi aliquid error veritatis consequuntur, excepturi cumque fuga eum incidunt doloremque?</p>
      <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. At qui enim rem totam voluptatum. Aut saepe temporibus, facilis ex a iste expedita minima dolorum dicta doloribus libero aliquid, quae maxime? Lorem ipsum dolor sit amet consectetur adipisicing elit. Fugiat suscipit beatae eum, est soluta ducimus ratione et impedit sapiente, nihil, atque dignissimos adipisci? Totam atque officia quis voluptates sed veniam?</p>
      <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Expedita voluptates quisquam possimus tenetur, dicta enim rerum quis, quaerat id nobis provident quo dolorum sapiente temporibus facere non repellendus consequatur cupiditate!</p>
      <h3>Details</h3>
      <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quae laboriosam distinctio atque sint earum? Temporibus, voluptas aspernatur aliquam nisi sed harum laborum, nemo odio animi officia quisquam. Veniam, natus reprehenderit.</p>
    </div>

    <footer class="border-top text-center small text-muted py-3">
      <p><a href="/" class="mx-1">Home</a> | <a class="mx-1" href="/about-us">About Us</a> | <a class="mx-1" href="/terms">Terms</a></p>
      <p class="m-0">Copyright &copy; 2025 <a href="/" class="text-muted">ComplexApp</a>. All rights reserved.</p>
    </footer>
  </body>
</html>


================================================
FILE: previewDist.js
================================================
const express = require("express")
const path = require("path")
const app = new express()
app.use(express.static(path.join(__dirname, "dist")))
app.get("*", (req, res) => res.sendFile(__dirname + "/dist/index.html"))
app.listen("4000")


================================================
FILE: vscode-react-component.txt
================================================
"React Component": {
		"prefix": "rc",
		"body": [
			"import React, { useEffect } from \"react\"",
			"",
			"function ${1:ComponentName}() {",
			"  return (",
			"    <>",
			"      $2",
			"    </>",
			"  )",
			"}",
			"",
			"export default ${1:ComponentName}"
		],
		"description": "React Component"
	}
Download .txt
gitextract_v6qlikgs/

├── .gitignore
├── 01-webpack-starter.txt
├── 02-webpack-end.txt
├── backend-api/
│   ├── Procfile
│   ├── app.js
│   ├── controllers/
│   │   ├── followController.js
│   │   ├── postController.js
│   │   └── userController.js
│   ├── db.js
│   ├── models/
│   │   ├── Follow.js
│   │   ├── Post.js
│   │   └── User.js
│   ├── package.json
│   └── router.js
├── generateHtml.js
├── html-templates/
│   ├── about.html
│   ├── chat-is-visible.html
│   ├── create-post.html
│   ├── index-empty-feed.html
│   ├── index-feed.html
│   ├── index-guest.html
│   ├── main.css
│   ├── profile-followers.html
│   ├── profile-posts.html
│   ├── search-is-visible.html
│   ├── single-post.html
│   └── terms.html
├── previewDist.js
└── vscode-react-component.txt
Download .txt
SYMBOL INDEX (3 symbols across 2 files)

FILE: backend-api/db.js
  function start (line 7) | async function start() {

FILE: generateHtml.js
  function Shell (line 10) | function Shell() {
  method onAllReady (line 60) | onAllReady() {
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (88K chars).
[
  {
    "path": ".gitignore",
    "chars": 281,
    "preview": ".env\n*.diff\n*.err\n*.orig\n*.log\n*.rej\n*.swo\n*.swp\n*.vi\n*~\n*.sass-cache\n.stylelintcache\nnode_modules/\n.tmp/\nyarn.lock\n.DS_"
  },
  {
    "path": "01-webpack-starter.txt",
    "chars": 718,
    "preview": "const path = require(\"path\")\n\nmodule.exports = {\n  entry: \"./app/Main.js\",\n  output: {\n    publicPath: \"/\",\n    path: pa"
  },
  {
    "path": "02-webpack-end.txt",
    "chars": 2216,
    "preview": "const currentTask = process.env.npm_lifecycle_event\nconst path = require(\"path\")\nconst Dotenv = require(\"dotenv-webpack\""
  },
  {
    "path": "backend-api/Procfile",
    "chars": 15,
    "preview": "web: node db.js"
  },
  {
    "path": "backend-api/app.js",
    "chars": 822,
    "preview": "const express = require(\"express\")\nconst app = express()\nconst sanitizeHTML = require(\"sanitize-html\")\nconst jwt = requi"
  },
  {
    "path": "backend-api/controllers/followController.js",
    "chars": 510,
    "preview": "const Follow = require(\"../models/Follow\")\n\nexports.apiAddFollow = function(req, res) {\n  let follow = new Follow(req.pa"
  },
  {
    "path": "backend-api/controllers/postController.js",
    "chars": 1444,
    "preview": "const Post = require(\"../models/Post\")\n\nexports.apiCreate = function(req, res) {\n  let post = new Post(req.body, req.api"
  },
  {
    "path": "backend-api/controllers/userController.js",
    "chars": 4283,
    "preview": "const User = require(\"../models/User\")\nconst Post = require(\"../models/Post\")\nconst Follow = require(\"../models/Follow\")"
  },
  {
    "path": "backend-api/db.js",
    "chars": 304,
    "preview": "const dotenv = require(\"dotenv\")\ndotenv.config()\nconst { MongoClient } = require(\"mongodb\")\n\nconst client = new MongoCli"
  },
  {
    "path": "backend-api/models/Follow.js",
    "chars": 4370,
    "preview": "const usersCollection = require(\"../db\").db().collection(\"users\")\nconst followsCollection = require(\"../db\").db().collec"
  },
  {
    "path": "backend-api/models/Post.js",
    "chars": 5460,
    "preview": "const postsCollection = require(\"../db\").db().collection(\"posts\")\nconst followsCollection = require(\"../db\").db().collec"
  },
  {
    "path": "backend-api/models/User.js",
    "chars": 4573,
    "preview": "const bcrypt = require(\"bcryptjs\")\nconst usersCollection = require('../db').db().collection(\"users\")\nconst validator = r"
  },
  {
    "path": "backend-api/package.json",
    "chars": 749,
    "preview": "{\n  \"name\": \"complex-app\",\n  \"version\": \"1.0.0\",\n  \"description\": \"hey dude\",\n  \"main\": \"db.js\",\n  \"repository\": {\n    \""
  },
  {
    "path": "backend-api/router.js",
    "chars": 2045,
    "preview": "const apiRouter = require(\"express\").Router()\nconst userController = require(\"./controllers/userController\")\nconst postC"
  },
  {
    "path": "generateHtml.js",
    "chars": 2349,
    "preview": "import React from \"react\"\nimport ReactDOMServer from \"react-dom/server\"\nimport fs from \"fs\"\nimport Footer from \"./app/co"
  },
  {
    "path": "html-templates/about.html",
    "chars": 3153,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/chat-is-visible.html",
    "chars": 5014,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/create-post.html",
    "chars": 2962,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/index-empty-feed.html",
    "chars": 2663,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/index-feed.html",
    "chars": 3564,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/index-guest.html",
    "chars": 3827,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/main.css",
    "chars": 9735,
    "preview": "body {\n  font-family: \"Public Sans\", sans-serif;\n}\n\n.header-bar {\n  background-color: #292929;\n}\n\n.container--narrow {\n "
  },
  {
    "path": "html-templates/profile-followers.html",
    "chars": 3785,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/profile-posts.html",
    "chars": 3726,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/search-is-visible.html",
    "chars": 5697,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/single-post.html",
    "chars": 3550,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "html-templates/terms.html",
    "chars": 3436,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "previewDist.js",
    "chars": 236,
    "preview": "const express = require(\"express\")\nconst path = require(\"path\")\nconst app = new express()\napp.use(express.static(path.jo"
  },
  {
    "path": "vscode-react-component.txt",
    "chars": 310,
    "preview": "\"React Component\": {\n\t\t\"prefix\": \"rc\",\n\t\t\"body\": [\n\t\t\t\"import React, { useEffect } from \\\"react\\\"\",\n\t\t\t\"\",\n\t\t\t\"function "
  }
]

About this extraction

This page contains the full source code of the LearnWebCode/react-course GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (79.9 KB), approximately 23.5k tokens, and a symbol index with 3 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!