diff --git a/Gemfile b/Gemfile
index b1a320395a..1b7eb4376e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -11,3 +11,14 @@ end
group :development, :test do
gem 'rubocop', '1.20'
end
+
+gem "pg", "~> 1.4"
+
+gem "sinatra", "~> 3.0"
+gem "sinatra-contrib", "~> 3.0"
+gem "webrick", "~> 1.8"
+gem "rack-test", "~> 2.1"
+
+gem "rackup", "~> 1.0"
+
+gem "bcrypt", "~> 3.1"
diff --git a/Gemfile.lock b/Gemfile.lock
index 66064703c7..f431713add 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -3,11 +3,24 @@ GEM
specs:
ansi (1.5.0)
ast (2.4.2)
+ bcrypt (3.1.18)
diff-lcs (1.4.4)
docile (1.4.0)
+ multi_json (1.15.0)
+ mustermann (3.0.0)
+ ruby2_keywords (~> 0.0.1)
parallel (1.20.1)
parser (3.0.2.0)
ast (~> 2.4.1)
+ pg (1.4.6)
+ rack (2.2.6.4)
+ rack-protection (3.0.5)
+ rack
+ rack-test (2.1.0)
+ rack (>= 1.3)
+ rackup (1.0.0)
+ rack (< 3)
+ webrick
rainbow (3.0.0)
regexp_parser (2.1.1)
rexml (3.2.5)
@@ -36,6 +49,7 @@ GEM
rubocop-ast (1.11.0)
parser (>= 3.0.1.1)
ruby-progressbar (1.11.0)
+ ruby2_keywords (0.0.5)
simplecov (0.21.2)
docile (~> 1.1)
simplecov-html (~> 0.11)
@@ -46,18 +60,41 @@ GEM
terminal-table
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.3)
+ sinatra (3.0.5)
+ mustermann (~> 3.0)
+ rack (~> 2.2, >= 2.2.4)
+ rack-protection (= 3.0.5)
+ tilt (~> 2.0)
+ sinatra-contrib (3.0.5)
+ multi_json
+ mustermann (~> 3.0)
+ rack-protection (= 3.0.5)
+ sinatra (= 3.0.5)
+ tilt (~> 2.0)
terminal-table (3.0.1)
unicode-display_width (>= 1.1.1, < 3)
+ tilt (2.1.0)
unicode-display_width (2.0.0)
+ webrick (1.8.1)
PLATFORMS
ruby
+ x86_64-darwin-20
+ x86_64-darwin-21
+ x86_64-linux
DEPENDENCIES
+ bcrypt (~> 3.1)
+ pg (~> 1.4)
+ rack-test (~> 2.1)
+ rackup (~> 1.0)
rspec
rubocop (= 1.20)
simplecov
simplecov-console
+ sinatra (~> 3.0)
+ sinatra-contrib (~> 3.0)
+ webrick (~> 1.8)
RUBY VERSION
ruby 3.0.2p107
diff --git a/README.md b/README.md
index 465eda879b..d45bd47d4a 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,25 @@
-Chitter Challenge
-=================
+# Chitter Challenge by Pablo Joyce
-* Feel free to use Google, your notes, books, etc. but work on your own
-* If you refer to the solution of another coach or student, please put a link to that in your README
-* If you have a partial solution, **still check in a partial solution**
-* You must submit a pull request to this repo with your code by 10am Monday morning
+[Getting Started](#getting-started) | [Using the application](#using-the-application) | [Running the tests](#running-the-tests) | [Design](#design) | [Screenshots](#screenshots) | [Technologies used](#technologies-used) | [Notes on test coverage](#notes-on-test-coverage)
-Challenge:
--------
+
+
+
+
+
+
+
-As usual please start by forking this repo.
+* This project is a solo Ruby project completed at the end of the web applications module of the Makers Academy bootcamp.
+* The application is built with the Sinatra web framework, rendering view files using ERB
+* The application uses a PostgresSQL database
+* The database connection is established in `lib/database_connection.rb` using the `pg` gem
+* Seed data for tests is included in `spec/seeds.sql`
-We are going to write a small Twitter clone that will allow the users to post messages to a public stream.
+## The Brief
-Features:
--------
+To create a basic Twitter clone that will allow the users to post messages.
+We were given the following user stories as a starting point:
```
STRAIGHT UP
@@ -52,61 +57,130 @@ So that I can stay constantly tapped in to the shouty box of Chitter
I want to receive an email if I am tagged in a Peep
```
-Technical Approach:
------
+We were given the following notes on functionality:
-In the last two weeks, you integrated a database using the `pg` gem and Repository classes. You also implemented small web applications using Sinatra, RSpec, HTML and ERB views to make dynamic webpages. You can continue to use this approach when building Chitter Challenge.
+* You don't have to be logged in to see the peeps.
+* Makers sign up to chitter with their email, password, name and a username (e.g. samm@makersacademy.com, password123, Sam Morgan, sjmog).
+* The username and email are unique.
+* Peeps (posts to chitter) have the name of the maker and their user handle.
-You can refer to the [guidance on Modelling and Planning a web application](https://github.com/makersacademy/web-applications/blob/main/pills/modelling_and_planning_web_application.md), to help you in planning the different web pages you will need to implement this challenge. If you'd like to deploy your app to Heroku so other people can use it, [you can follow this guidance](https://github.com/makersacademy/web-applications/blob/main/html_challenges/07_deploying.md).
+## Getting Started
+[Top](#chitter-challenge-by-pablo-joyce) | [Use](#using-the-application) | [Tests](#running-the-tests) | [Design](#design) | [Screenshots](#screenshots) | [Tech](#technologies-used) | [Coverage](#notes-on-test-coverage)
-If you'd like more technical challenge now, try using an [Object Relational Mapper](https://en.wikipedia.org/wiki/Object-relational_mapping) as the database interface, instead of implementing your own Repository classes.
+At time of writing, this project has a version deployed on Render at [https://chitter-7pam.onrender.com/](https://chitter-7pam.onrender.com/). Please note that this may take 30 seconds or longer to 'spin up' as it is deployed on a free tier of Render.
+At some point in the future, this version may be taken down as Render only allows a single database instance on the free tier.
-Some useful resources:
-**Ruby Object Mapper**
-- [ROM](https://rom-rb.org/)
+For the local version, clone this repository to your local machine:
+`git clone https://github.com/pablisch/chitter-challenge.git`
-**ActiveRecord**
-- [ActiveRecord ORM](https://guides.rubyonrails.org/active_record_basics.html)
-- [Sinatra & ActiveRecord setup](https://learn.co/lessons/sinatra-activerecord-setup)
+Switch to the `local-server` branch:
+`git switch local-server`
-Notes on functionality:
-------
+cd to the project and install dependencies:
+`bundle install`
-* You don't have to be logged in to see the peeps.
-* Makers sign up to chitter with their email, password, name and a username (e.g. samm@makersacademy.com, password123, Sam Morgan, sjmog).
-* The username and email are unique.
-* Peeps (posts to chitter) have the name of the maker and their user handle.
-* Your README should indicate the technologies used, and give instructions on how to install and run the tests.
+Ensure that your postgres server is accessable at the IP address 127.0.0.1.
+Create the postgreSQL databases for running and testing the project:
+```bash
+createdb chitter
+createdb chitter_test
+```
+Create the tables in the databases:
+```bash
+psql -h 127.0.0.1 chitter < spec/tables.sql
+psql -h 127.0.0.1 chitter_test < spec/tables.sql
+```
+Populate the test database with seed data:
+```bash
+psql -h 127.0.0.1 chitter < spec/seeds.sql
+psql -h 127.0.0.1 chitter_test < spec/seeds.sql
+```
+Start the development server:
+
+`rackup`
+
+Access the website in your browser at [localhost:9292](http://localhost:9292/).
+
+## Using the application
+[Top](#chitter-challenge-by-pablo-joyce) | [Getting Started](#getting-started) | [Tests](#running-the-tests) | [Design](#design) | [Screenshots](#screenshots) | [Tech](#technologies-used) | [Coverage](#notes-on-test-coverage)
+
+Be default uses can see all messages posted by all users, but cannot post messages.
+
+Follow the link to register as a user, you will then be prompted to redirect to login.
-Bonus:
------
+After login, you will be redirected to the home page where you can post a new message. messages posted by all users will appear here in reverse chronological order.
-If you have time you can implement the following:
+## Running the tests
-* In order to start a conversation as a maker I want to reply to a peep from another maker.
+`rspec`
-And/Or:
+All tests should pass with a total code coverage of **99.41%**.
-* Work on the CSS to make it look good.
+Screenshots of the tests and coverage can be found in the [Coverage](#notes-on-test-coverage) section of this readme.
-Good luck and let the chitter begin!
+## Design
+[Top](#chitter-challenge-by-pablo-joyce) | [Getting Started](#getting-started) | [Use](#using-the-application) | [Tests](#running-the-tests) | [Screenshots](#screenshots) | [Tech](#technologies-used) | [Coverage](#notes-on-test-coverage)
-Code Review
------------
+The design planning used at the time of making can be seen in recipes/chitter_plan.md.
-In code review we'll be hoping to see:
+I started by creating a [diagram of the user experience]([.images/chitter3.png](https://github.com/pablisch/chitter-challenge/blob/main/images/chitter3.png?raw=true)) and incorporated the routes and validations into this diagram:
-* All tests passing
-* High [Test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) (>95% is good)
-* The code is elegant: every class has a clear responsibility, methods are short etc.
+![chitter user experience diagram](./images/chitter3.png)
-Reviewers will potentially be using this [code review rubric](docs/review.md). Referring to this rubric in advance may make the challenge somewhat easier. You should be the judge of how much challenge you want at this moment.
+The design itself changed a lot during the design process as the diagramming revealed issues and solved many problems before I started coding.
+This gave me a good roadmap from which to build the app.
-Notes on test coverage
-----------------------
+* The application is built with the Sinatra web framework, rendering view files using ERB
+* Each database table has a corresponding Ruby class defined with a singular version of the name of the table.
+* Each table has an additional 'TABLENAME_repository' class with methods that allow for CRUD operations on the DB.
+* The database connection is established in `lib/database_connection.rb` using the `pg` gem
+* The database tables have a one-to-many relationship with the users table. This is implemented by storing the user_id in the peeps table.
+* Route handling occurs in the `Application` class defined in [./app.rb](./app.rb). [./config.ru](./config.ru) executes the code inside of `Application` when `rackup` is run at the command line.
+* Passwords are hashed using the [BCrypt](https://rubygems.org/gems/bcrypt/versions/3.1.12) gem.
-Please ensure you have the following **AT THE TOP** of your spec_helper.rb in order to have test coverage stats generated
-on your pull request:
+## Screenshots
+[Top](#chitter-challenge-by-pablo-joyce) | [Getting Started](#getting-started) | [Use](#using-the-application) | [Tests](#running-the-tests) | [Design](#design) | [Tech](#technologies-used) | [Coverage](#notes-on-test-coverage)
+
+Register a new user:
+
+
+
+Main screen when logged in:
+
+
+
+Create a new peep:
+
+
+
+Display new peep:
+
+
+
+## Technologies used
+[Top](#chitter-challenge-by-pablo-joyce) | [Getting Started](#getting-started) | [Use](#using-the-application) | [Tests](#running-the-tests) | [Design](#design) | [Screenshots](#screenshots) | [Coverage](#notes-on-test-coverage)
+
+This project uses:
+* Ruby
+* PostgreSQL
+* ERB
+* Rackup
+* Sinatra
+* Webrick
+* Git
+* GitHub
+* Bcrypt
+
+In development, this project also used:
+* Postman
+* TablePlus
+* RSpec
+
+## Notes on test coverage
+[Top](#chitter-challenge-by-pablo-joyce) | [Getting Started](#getting-started) | [Use](#using-the-application) | [Tests](#running-the-tests) | [Design](#design) | [Screenshots](#screenshots) | [Tech](#technologies-used)
+
+The following code **at the top** of spec_helper.rb causes test coverage stats to be generated
+on pull request:
```ruby
require 'simplecov'
@@ -120,4 +194,10 @@ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
SimpleCov.start
```
-You can see your test coverage when you run your tests. If you want this in a graphical form, uncomment the `HTMLFormatter` line and see what happens!
+Coverage:
+
+
+
+Test results:
+
+
diff --git a/app.rb b/app.rb
new file mode 100644
index 0000000000..31678a084d
--- /dev/null
+++ b/app.rb
@@ -0,0 +1,155 @@
+require_relative 'lib/database_connection'
+require 'sinatra/base'
+require 'sinatra/reloader'
+require_relative 'lib/user_repository'
+require_relative 'lib/peep_repository'
+require 'bcrypt'
+
+DatabaseConnection.connect
+
+class Application < Sinatra::Base
+
+ enable :sessions # allows users sessions
+
+ # This allows the app code to refresh
+ # without having to restart the server.
+ configure :development do
+ register Sinatra::Reloader
+ also_reload 'lib/user_repository'
+ also_reload 'lib/peep_repository'
+ end
+
+ get '/' do
+ repo = PeepRepository.new
+ peeps = repo.all_with_names
+ @peep_info = peeps.map{ |peep| [peep.username, peep.time, peep.body, peep.tags, peep.name]}.reverse
+ return erb(:index)
+ end
+
+ get '/peeps' do
+ @user_id = session[:user_id]
+ return erb(:add_peep)
+ end
+
+ post '/peeps' do
+ @body, @tags, @user_id = params[:body], params[:tags],params[:user_id]
+ script_check([@body, @tags, @user_id], '/peeps')
+ validate_string(@body, "peep", '/peeps')
+ repo = PeepRepository.new
+ new_peep = Peep.new
+ new_peep.body = @body
+ new_peep.time = Time.now#.strftime("%Y-%m-%d %T")
+ new_peep.tags = @tags
+ new_peep.user_id = @user_id
+ repo.create(new_peep)
+ return redirect('/')
+ end
+
+ get '/register' do
+ return erb(:register)
+ end
+
+ post '/register' do
+ @name, @username, @email, @password = params[:name], params[:username], params[:email], params[:password]
+ script_check([@name, @username, @email, @password], '/register')
+ validate_string(@name, "name", '/register')
+ validate_string(@username, "username", '/register')
+ validate_email(@email)
+ username_email_unique(@username, @email)
+ validate_password(@password)
+ user_repo = UserRepository.new
+ new_user = User.new
+ new_user.name = @name
+ new_user.username = @username
+ new_user.email = @email
+ new_user.password = @password
+ user_repo.create(new_user)
+ user = user_repo.find_by_email(@email)
+ login_user(user)
+ return redirect('/')
+ end
+
+ get '/login' do
+ return erb(:login)
+ end
+
+ post '/login' do
+ @email = params[:email]
+ @password = params[:password]
+ script_check([@email, @password], '/login')
+ email_exists(@email)
+ user = UserRepository.new.find_by_email(@email)
+ email_password_match(user, @password)
+ return redirect('/')
+ end
+
+ get '/logout' do
+ session.clear
+ return redirect('/')
+ end
+
+ helpers do
+ def validate_string(name, field, redirect_path)
+ unless name.match?(/[a-zA-Z]/)
+ session[:error] = "Invalid #{field}: must contain one or more letters.\n"
+ return redirect(redirect_path)
+ end
+ end
+
+ def validate_email(email)
+ unless email =~ URI::MailTo::EMAIL_REGEXP
+ session[:error] = "Invalid email: please enter a valid email to register.\n"
+ return redirect('register')
+ end
+ end
+
+ def validate_password(password)
+ unless password.match(/^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,}$/)
+ session[:error] = "Invalid password: minimum eight characters and contain at least one lowercase letter, uppercase letter and digit.\n"
+ return redirect('register')
+ end
+ end
+
+ def username_email_unique(username, email)
+ if UserRepository.new.all_usernames.include?(username)
+ session[:error] = "That username is already taken.\n"
+ return redirect('register')
+ end
+ if UserRepository.new.all_emails.include?(email)
+ session[:error] = "That email is already registered to a user.\n"
+ return redirect('register')
+ end
+ end
+
+ def email_exists(email)
+ emails = UserRepository.new.all_emails
+ unless emails.include?(email)
+ session[:error] = "Email and password do not match any registered user.\n"
+ return redirect('login')
+ end
+ end
+
+ def email_password_match(user, entered_password)
+ stored_password = BCrypt::Password.new(user.password)
+ unless stored_password == entered_password # i.e. UNLESS ==
+ session[:user_id] = nil
+ session[:error] = "Email and password do not match any registered user.\n"
+ return redirect('/login')
+ end
+ login_user(user)
+ end
+
+ def login_user(user)
+ session[:username] = user.username
+ session[:user_id] = user.id
+ end
+
+ def script_check(inputs_array, redirect_path)
+ if inputs_array.join.match?(/[<>\/]/)
+ session[:error] = "'<', '>' and '/' are not permitted characters.\n"
+ return redirect(redirect_path)
+ end
+ end
+
+ end
+end
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000000..30570ef105
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,2 @@
+require './app'
+run Application
\ No newline at end of file
diff --git a/images/chitter3.png b/images/chitter3.png
new file mode 100644
index 0000000000..0df6b3cd95
Binary files /dev/null and b/images/chitter3.png differ
diff --git a/images/chitter_design.png b/images/chitter_design.png
new file mode 100644
index 0000000000..8dddc15559
Binary files /dev/null and b/images/chitter_design.png differ
diff --git a/images/coverage.png b/images/coverage.png
new file mode 100644
index 0000000000..a35d24bf3e
Binary files /dev/null and b/images/coverage.png differ
diff --git a/images/create.png b/images/create.png
new file mode 100644
index 0000000000..b61c45b81e
Binary files /dev/null and b/images/create.png differ
diff --git a/images/logged-in.png b/images/logged-in.png
new file mode 100644
index 0000000000..7fd3016ca0
Binary files /dev/null and b/images/logged-in.png differ
diff --git a/images/new.png b/images/new.png
new file mode 100644
index 0000000000..f45c30c3b7
Binary files /dev/null and b/images/new.png differ
diff --git a/images/register.png b/images/register.png
new file mode 100644
index 0000000000..a2960a7261
Binary files /dev/null and b/images/register.png differ
diff --git a/images/tests.png b/images/tests.png
new file mode 100644
index 0000000000..ea95b72fda
Binary files /dev/null and b/images/tests.png differ
diff --git a/lib/database_connection.rb b/lib/database_connection.rb
new file mode 100644
index 0000000000..f0c506fab0
--- /dev/null
+++ b/lib/database_connection.rb
@@ -0,0 +1,48 @@
+require 'pg'
+
+class DatabaseConnection
+ # This method connects to PostgreSQL using the PG gem. We connect to 127.0.0.1
+
+ # 👇🏻 ################# RENDER connect method
+
+ # def self.connect
+ # # If the environment variable (set by Render)
+ # # is present, use this to open the connection.
+ # if ENV['DATABASE_URL'] != nil
+ # @connection = PG.connect(ENV['DATABASE_URL'])
+ # return
+ # end
+
+ # if ENV['ENV'] == 'test'
+ # database_name = 'chitter_test'
+ # else
+ # database_name = 'chitter_render_db'
+ # end
+ # @connection = PG.connect({ host: '127.0.0.1', dbname: database_name })
+ # end
+
+
+
+
+ # 👇🏻 ################## local connect method - NOT for RENDER
+
+ def self.connect
+ if ENV['ENV'] == 'test'
+ database_name = 'chitter_test'
+ else
+ database_name = 'chitter'
+ end
+ @connection = PG.connect({ host: '127.0.0.1', dbname: database_name })
+ end
+
+ # 👇🏻 ################## Needed for local AND Render - Do NOT comment out
+
+ def self.exec_params(query, params)
+ if @connection.nil?
+ raise 'DatabaseConnection.exec_params: Cannot run a SQL query as the connection to'\
+ 'the database was never opened. Did you make sure to call first the method '\
+ '`DatabaseConnection.connect` in your app.rb file (or in your tests spec_helper.rb)?'
+ end
+ @connection.exec_params(query, params)
+ end
+end
\ No newline at end of file
diff --git a/lib/peep.rb b/lib/peep.rb
new file mode 100644
index 0000000000..aac7d8023b
--- /dev/null
+++ b/lib/peep.rb
@@ -0,0 +1,7 @@
+class Peep
+ attr_accessor :id, :body, :time, :tags, :user_id, :username, :name
+
+ # def initialize
+ # @tags = []
+ # end
+end
\ No newline at end of file
diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb
new file mode 100644
index 0000000000..00f828bdf4
--- /dev/null
+++ b/lib/peep_repository.rb
@@ -0,0 +1,53 @@
+require_relative './peep'
+require_relative './user'
+require 'time'
+
+class PeepRepository
+ def all # return all peeps
+ sql = 'SELECT * FROM peeps;'
+ results = DatabaseConnection.exec_params(sql, [])
+ peeps = []
+ results.each{ |record| peeps << peep_builder(record) }
+ return peeps
+ end
+
+ def all_with_names
+ sql = 'SELECT users.name, users.username, peeps.body, peeps.time, peeps.tags, peeps.user_id
+ FROM users
+ JOIN peeps
+ ON peeps.user_id = users.id;'
+ results = DatabaseConnection.exec_params(sql, [])
+ peeps_with_names = []
+ results.each do |record|
+ peep = Peep.new
+ peep.id = record['id'].to_i
+ peep.body = record['body']
+ peep.time = (record['time'])
+ peep.tags = record['tags']
+ peep.user_id = record['user_id'].to_i
+ peep.name = record['name']
+ peep.username = record['username']
+ peeps_with_names << peep
+ end
+ return peeps_with_names
+ end
+
+ def create(peep)
+ sql = 'INSERT INTO peeps (body, time, tags, user_id) VALUES ($1, $2, $3, $4);'
+ params = [peep.body, peep.time, peep.tags, peep.user_id]
+ DatabaseConnection.exec_params(sql, params)
+ end
+
+ private
+
+ def peep_builder(record) # used to build peeps in iteration by #all
+ peep = Peep.new
+ peep.id = record['id'].to_i
+ peep.body = record['body']
+ peep.time = record['time']
+ peep.tags = record['tags']
+ peep.user_id = record['user_id'].to_i
+ return peep
+ end
+end
+
diff --git a/lib/user.rb b/lib/user.rb
new file mode 100644
index 0000000000..11a95fcc76
--- /dev/null
+++ b/lib/user.rb
@@ -0,0 +1,3 @@
+class User
+ attr_accessor :id, :name, :username, :email, :password
+end
\ No newline at end of file
diff --git a/lib/user_repository.rb b/lib/user_repository.rb
new file mode 100644
index 0000000000..b097a05cc5
--- /dev/null
+++ b/lib/user_repository.rb
@@ -0,0 +1,57 @@
+require_relative './user'
+require 'bcrypt'
+
+class UserRepository
+ def all
+ sql = 'SELECT * FROM users;'
+ results = DatabaseConnection.exec_params(sql, [])
+ users = []
+ results.each{ |record| users << user_builder(record) }
+ return users
+ end
+
+ def create(user)
+ sql = 'INSERT INTO users (name, username, email, password) VALUES ($1, $2, $3, $4);'
+ password_hash = BCrypt::Password.create(user.password)
+ params = [user.name, user.username, user.email, password_hash]
+ DatabaseConnection.exec_params(sql, params)
+ end
+
+ def find_by_email(email)
+ sql = 'SELECT * FROM users WHERE email = $1;'
+ params = [email]
+ results = DatabaseConnection.exec_params(sql, params)
+ record = results[0]
+ this_user = user_builder(record)
+ return this_user
+ end
+
+ def all_usernames
+ sql = 'SELECT username FROM users;'
+ results = DatabaseConnection.exec_params(sql, [])
+ usernames = []
+ results.each{ |record| record.each{ |key, username| usernames << username }}
+ return usernames
+ end
+
+ def all_emails
+ sql = 'SELECT email FROM users;'
+ results = DatabaseConnection.exec_params(sql, [])
+ emails = []
+ results.each{ |record| record.each{ |key, email| emails << email }}
+ return emails
+ end
+
+ private
+
+ def user_builder(record)
+ user = User.new
+ user.id = record['id'].to_i
+ user.name = record['name']
+ user.username = record['username']
+ user.email = record['email']
+ user.password = record['password']
+ return user
+ end
+
+end
\ No newline at end of file
diff --git a/public/peep.png b/public/peep.png
new file mode 100644
index 0000000000..16d3b0cc52
Binary files /dev/null and b/public/peep.png differ
diff --git a/public/peepicon.png b/public/peepicon.png
new file mode 100644
index 0000000000..311f383eba
Binary files /dev/null and b/public/peepicon.png differ
diff --git a/public/styles.css b/public/styles.css
new file mode 100644
index 0000000000..2f946225d8
--- /dev/null
+++ b/public/styles.css
@@ -0,0 +1,186 @@
+@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@400;700&display=swap');
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: 'Raleway', sans-serif;
+ font-size: 16px;
+ line-height: 1.5;
+ color: #fff;
+ background: #a1c3ff;
+}
+
+.container {
+ max-width: 820px;
+ margin: 0 auto;
+ padding: 0 30px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+img {
+ max-width: 60px;
+}
+
+nav {
+ background-color: #3474e6;
+ height: 80px;
+}
+
+.navbar {
+ height: 100%;
+}
+
+.list {
+ display: flex;
+}
+
+.logo {
+ display: flex;
+ align-items: center;
+}
+
+.logo h1 {
+ color: #accef4;
+ font-size: 45px;
+ margin: 0 40px 0 10px;
+}
+
+.logo h1:hover {
+ color: #3e7ce8;
+ text-shadow: 1px 1px 6px #8fbbed;
+}
+
+a {
+ color: #fff;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+a:hover {
+ color: #76e4e4;
+}
+
+.list p {
+ margin-left: 20px;
+}
+
+header {
+ background-color: #0151cc;
+}
+
+header .container {
+ padding-top: 30px;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+header h1 {
+ color: #8fbbed;
+}
+
+header h2 {
+ color: #76a9e4;
+}
+
+header
+
+.error {
+ padding-bottom: 15px;
+}
+
+.right-align {
+ text-align: right;
+}
+
+.panel {
+ min-width: 750px;
+ background-color: #0151cc;
+ border-radius: 10px;
+ margin: 20px 10px 0;
+ box-shadow: 0 3px 5px rgba(0,0,0,0.6);
+ padding: 20px 30px 0;
+}
+
+.panel p {
+ color: #8fbbed;
+}
+
+#smallprint {
+ color: #76a9e4;
+ padding-bottom: 50px;
+}
+
+input {
+ margin-bottom: 5px;
+}
+
+.button {
+ background-color: #aac7fb;
+ color: #003687;
+ border: none;
+ padding: 5px 10px;
+ border-radius: 4px;
+ border: 1px solid #0143a5;
+ font-size: 14px;
+ cursor: pointer;
+}
+
+.button:hover {
+ background-color: #9ae0e0;
+ color: #11554f;
+ box-shadow: 1px 3px 7px rgba(5, 66, 50, 0.6);
+}
+
+.text {
+ font-family: 'Raleway', sans-serif;
+ width: 200px;
+ padding: 3px;
+ margin: 0 0 10px 10px;
+ border-radius: 5px;
+ border: 1px solid #ccc;
+}
+
+#tag-text {
+ margin: 0 0 5px 0;
+}
+
+.textarea {
+ margin-top: 5px;
+ font-family: 'Raleway', sans-serif;
+ width: 300px;
+ padding: 3px;
+ border-radius: 5px;
+ border: 1px solid #ccc;
+}
+
+@media (max-width: 900px) {
+ .panel {
+ min-width: 650px;
+ }
+}
+
+@media (max-width: 800px) {
+ .panel {
+ min-width: 550px;
+ }
+ .logo h1 {
+ display: none;
+ }
+ nav h3 {
+ margin-left: 30px;
+ }
+}
+
+@media (max-width: 700px) {
+ .panel {
+ min-width: 400px;
+ }
+}
+
+
diff --git a/recipes/chitter_plan.md b/recipes/chitter_plan.md
new file mode 100644
index 0000000000..2c98d91841
--- /dev/null
+++ b/recipes/chitter_plan.md
@@ -0,0 +1,110 @@
+# Modelling and Planning for Chitter Challenge
+
+## Step 0: User stories or specification
+
+> As a Maker
+> So that I can let people know what I am doing
+> I want to {post} a message ([peep]) to chitter
+
+> As a maker
+> So that I can see what others are saying
+> I want to {see} all peeps in [reverse chronological order]
+
+> As a Maker
+> So that I can better appreciate the context of a peep
+> I want to see the [time] at which it was made
+
+> As a Maker
+> So that I can post messages on Chitter as me
+> I want to {sign up} for Chitter
+
+>- As a Maker
+>- So that only I can post messages on Chitter as me
+>- I want to {log in} to Chitter
+
+>- As a Maker
+>- So that I can avoid others posting messages on Chitter as me
+>- I want to {log out} of Chitter
+
+>* As a Maker
+>* So that I can stay constantly tapped in to the shouty box of Chitter
+>* I want to {receive an email} if I am [tagged] in a Peep
+
+## Step 1: Planning pages
+
+Adopt the perspective of a user browsing your website. Pages - Links - Forms
+
+![](./images/../../images/chitter_design.png)
+
+## Step 2: Planning routes
+
+Remember that a web application flow is all based on the HTTP request-response cycle.
+
+* GET (/) [index.erb] displays peeps plus link buttons to register, login, logout and new peeps.
+* GET (register/new) [register.erb] displays a register form and links to login and homepage via cancel button.
+* POST (register) validates new user and redirects to the homepage logged in or returns back to [register.erb].
+* GET
+
+ * A page (such as the home page) is usually returned by a `GET` route. For example, the
+ home page (which is almost always the first route you'll start with), is usually `GET
+ /`.
+ * If the user navigates to a different page using a link, you will need a `GET` route for
+ this other page.
+ * If the user submits a form that leads to a different page, you will need a `POST` route
+ to submit this form to.
+
+From the pages planned above, here are the routes we could plan. Once again, this is only
+a possible list of routes for this case — you could come up with slightly different ones,
+which would still satisfy the initial user stories.
+
+```md
+# Page: list of entries (aka the homepage)
+
+## Request:
+GET /
+No parameters
+
+## Response (200 OK)
+HTML view with list of entries
+```
+
+```md
+# Page: see full entry
+
+## Request:
+GET /entries/:id
+With path parameter :id
+
+## Response (200 OK)
+HTML view with details of a single entry
+```
+
+```md
+# Page: add a new entry
+
+## Request:
+GET /entries/add
+No parameters
+
+## Response (200 OK)
+HTML view with form to submit new entry (to POST /entries)
+```
+
+```md
+# Page: new entry added
+
+## Request:
+POST /entries
+With parameters:
+ title="What a great Sunday"
+ content="..."
+
+## Response (200 OK)
+HTML view with confirmation message
+```
+
+## Step 3: Test-drive and implement
+
+You'll now test-drive and implement each route. If your application interacts with a
+database to store its data, you'll also have to test-drive and implement other layers of
+the program (such as Repository classes) as part of your work.
diff --git a/spec/crypt_seeds.sql b/spec/crypt_seeds.sql
new file mode 100644
index 0000000000..b09fae70f0
--- /dev/null
+++ b/spec/crypt_seeds.sql
@@ -0,0 +1,34 @@
+TRUNCATE TABLE users, peeps RESTART IDENTITY CASCADE;
+
+INSERT INTO users ("name", "username", "email", "password") VALUES
+('Amber Thompson', 'Amber', 'amber@example.com', '$2a$12$NIjKQzEB.9mtj9L44r1iFuT5iB1fB.T0akjAo6vMKR7qXQO2Lnsey'),
+('Billy Thompkins', 'Billy', 'billy@example.com', '$2a$12$bsI4g2Bhk6NYRf646u1Y7eGdD6PiSo9uDiAEuHJVxwOsO9fofoQJy'),
+('Caleb Tomlinson', 'Caleb', 'caleb@example.com', '$2a$12$lGByQ50Z9IYVRiZYFppefutqlvp/0TYIF7bZKMR9VARM/AXkTyGsi')
+;
+
+INSERT INTO peeps ("body", "time", "tags", "user_id") VALUES
+('Today I learned how to make SQL queries to get information from relational databases. It is so interesting! I cannnot wait to do something with this!', '2023-04-01 13:00:00', '#code, #render, #deploy, #amber', 1),
+('Today I ate a whole cheesecake by myself. It was really delicious but now I feel quite sick and full of cheesecake and regret.', '2023-04-02 14:00:00', '#food, #sick, #billy', 2),
+('Today I wrote a Chitter App so that I could share all my coding achievement with my friends!', '2023-04-03 15:00:00', '#code, #chitter, #amber', 1),
+('Today I watched some clouds drift by. It was quite beautiful.', '2023-04-04 16:00:00', '#dream, #caleb', 3),
+('Today I deployed my first app on Render with a PostgreSQL database.', '2023-04-05 17:00:00', '#new, #sql, #amber', 1),
+('Today I dreamed I was a cloud.', '2023-04-06 18:00:00', '#clouds, #caleb', 3),
+('Today I went to an all you can eat pizza place. The pizzas were pretty greasy and bland but it was all you can eat so I kept eating. The more I ate the more I felt a deep self-loathing but I could not stop and now I feel a deep sickness in my soul.', '2023-04-07 19:00:00', '#stuffed, #soul, #billy', 2)
+;
+
+-- Ensure that your postgres server is accessable at the IP address 127.0.0.1.
+-- Create the postgreSQL databases for running and testing the project:
+-- ```bash
+-- createdb chitter
+-- createdb chitter_test
+-- ```
+-- Create the tables in the databases:
+-- ```bash
+-- psql -h 127.0.0.1 chitter < spec/crypt_tables.sql
+-- psql -h 127.0.0.1 chitter_test < spec/tables.sql
+-- ```
+-- Populate the test database with seed data:
+-- ```bash
+-- psql -h 127.0.0.1 chitter < spec/crypt_seeds.sql
+-- psql -h 127.0.0.1 chitter_test < spec/seeds.sql
+-- ```
\ No newline at end of file
diff --git a/spec/crypt_tables.sql b/spec/crypt_tables.sql
new file mode 100644
index 0000000000..cc8556cf3f
--- /dev/null
+++ b/spec/crypt_tables.sql
@@ -0,0 +1,44 @@
+DROP TABLE IF EXISTS users, peeps;
+
+-- Table Definition
+CREATE TABLE users (
+ id SERIAL PRIMARY KEY,
+ name text,
+ username text,
+ email varchar,
+ password varchar
+);
+
+-- Table Definition
+CREATE TABLE peeps (
+ id SERIAL PRIMARY KEY,
+ body text,
+ time timestamp,
+ tags text,
+ user_id int
+);
+
+TRUNCATE TABLE users, peeps RESTART IDENTITY CASCADE;
+
+INSERT INTO users ("name", "username", "email", "password") VALUES
+('Amber Thompson', 'Amber', 'amber@email.com', '$2a$12$79o3wBPYwLiNGnS329LQJOV91pzp4RXaQi1KXPShy7bg6hBD1t3LO'),
+('Billy Thompkins', 'Billy', 'billy@email.com', '$2a$12$KuowH.RWU8/Hx7u1uNJAzOYEABy2APD9v4lrAGn5UxK9mhdXGSpXm'),
+('Caleb Tomlinson', 'Caleb', 'caleb@email.com', '$2a$12$a/Sg0TiHT7uXxsr4oWDfFu5DEzJrTDSPqkie3v3iYDcKOXb7D07j6')
+;
+
+INSERT INTO peeps ("body", "time", "tags", "user_id") VALUES
+('Today I coded', '2023-04-01 13:00:00', '#code, #amber', 1),
+('Today I ate', '2023-04-02 14:00:00', '#food, #billy', 2),
+('Today I slept', '2023-04-03 15:00:00', '#sleep, #amber', 1),
+('Today I wept', '2023-04-04 16:00:00', '#cry, #caleb', 3),
+('Today I swam', '2023-04-05 17:00:00', '#swim, #amber', 1),
+('Today I meditated', '2023-04-06 18:00:00', '#meditate, #caleb', 3),
+('Today I relaxed', '2023-04-07 19:00:00', '#relax, #billy', 2)
+;
+
+-- psql -h 127.0.0.1 chitter
+-- psql -h 127.0.0.1 chitter_test
+
+
+-- psql -h 127.0.0.1 chitter < spec/seeds.sql
+-- psql -h 127.0.0.1 chitter_test < spec/seeds.sql
diff --git a/spec/integration/application_spec.rb b/spec/integration/application_spec.rb
new file mode 100644
index 0000000000..d96856161f
--- /dev/null
+++ b/spec/integration/application_spec.rb
@@ -0,0 +1,289 @@
+require "spec_helper"
+require "rack/test"
+require_relative '../../app'
+require 'peep_repository'
+require 'user_repository'
+
+def reset_tables
+ seed_sql = File.read('spec/seeds.sql')
+ connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' })
+ connection.exec(seed_sql)
+end
+
+describe Application do
+ # This is so we can use rack-test helper methods.
+ include Rack::Test::Methods
+
+ before(:each) do
+ reset_tables
+ end
+
+ let(:app) { Application.new }
+
+ context "homepage" do
+ it "displays a list of existing peeps" do
+ response = get('/')
+ expect(response.status).to eq 200
+ expect(response.body).to include "Welcome to Chitter
"
+ expect(response.body).to include "Today I coded"
+ expect(response.body).to include "Today I relaxed"
+ end
+ end
+
+ context "new peep form" do
+ it "displays a form page where text can be entered and submitted" do
+ response = get('/peeps')
+ expect(response.status).to eq 200
+ expect(response.body).to include 'Spread your chit
'
+ expect(response.body).to include '