diff --git a/Gemfile b/Gemfile index b1a320395a..d3916863f8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -ruby '3.0.2' +ruby '3.2.1' group :test do gem 'rspec' @@ -9,5 +9,13 @@ group :test do end group :development, :test do - gem 'rubocop', '1.20' + gem 'rubocop', '1.51' end +gem "pg", "~> 1.3" +gem "activerecord" + +gem "sinatra", "~> 3.0" +gem "sinatra-contrib", "~> 3.0" +gem "webrick", "~> 1.8" +gem "rack-test", "~> 2.1" +gem 'bcrypt' diff --git a/Gemfile.lock b/Gemfile.lock index 66064703c7..d96e59176e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,15 +1,40 @@ GEM remote: https://rubygems.org/ specs: + activemodel (7.0.5) + activesupport (= 7.0.5) + activerecord (7.0.5) + activemodel (= 7.0.5) + activesupport (= 7.0.5) + activesupport (7.0.5) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) ansi (1.5.0) ast (2.4.2) + bcrypt (3.1.18) + concurrent-ruby (1.2.2) diff-lcs (1.4.4) docile (1.4.0) - parallel (1.20.1) - parser (3.0.2.0) + i18n (1.13.0) + concurrent-ruby (~> 1.0) + json (2.6.3) + minitest (5.18.0) + multi_json (1.15.0) + mustermann (3.0.0) + ruby2_keywords (~> 0.0.1) + parallel (1.23.0) + parser (3.2.2.1) ast (~> 2.4.1) - rainbow (3.0.0) - regexp_parser (2.1.1) + pg (1.5.3) + rack (2.2.7) + rack-protection (3.0.6) + rack + rack-test (2.1.0) + rack (>= 1.3) + rainbow (3.1.1) + regexp_parser (2.8.0) rexml (3.2.5) rspec (3.10.0) rspec-core (~> 3.10.0) @@ -24,18 +49,20 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-support (3.10.2) - rubocop (1.20.0) + rubocop (1.51.0) + json (~> 2.3) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.9.1, < 2.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.11.0) - parser (>= 3.0.1.1) - ruby-progressbar (1.11.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -46,21 +73,43 @@ GEM terminal-table simplecov-html (0.12.3) simplecov_json_formatter (0.1.3) + sinatra (3.0.6) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.0.6) + tilt (~> 2.0) + sinatra-contrib (3.0.6) + multi_json + mustermann (~> 3.0) + rack-protection (= 3.0.6) + sinatra (= 3.0.6) + tilt (~> 2.0) terminal-table (3.0.1) unicode-display_width (>= 1.1.1, < 3) - unicode-display_width (2.0.0) + tilt (2.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.4.2) + webrick (1.8.1) PLATFORMS ruby DEPENDENCIES + activerecord + bcrypt + pg (~> 1.3) + rack-test (~> 2.1) rspec - rubocop (= 1.20) + rubocop (= 1.51) simplecov simplecov-console + sinatra (~> 3.0) + sinatra-contrib (~> 3.0) + webrick (~> 1.8) RUBY VERSION - ruby 3.0.2p107 + ruby 3.2.1p31 BUNDLED WITH - 2.2.26 + 2.4.13 diff --git a/README.md b/README.md index 465eda879b..49841adfac 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,17 @@ -Chitter Challenge +Chitter ================= -* 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 +Recreation of Twitter using Ruby and ActiveRecord. **Tools used**: Ruby, ActiveRecord, PostGreSQL, HTML, CSS, RSpec, Rubocop. -Challenge: -------- +# Build Instructions -As usual please start by forking this repo. +Clone the repo and then cd into the directory. -We are going to write a small Twitter clone that will allow the users to post messages to a public stream. +Run ```bundle install``` in terminal. -Features: -------- +After install is complete, run ```rackup``` -``` -STRAIGHT UP +Now you should be able to navigate to `localhost:9292` to display the site. -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 - -HARDER - -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 - -ADVANCED - -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 -``` - -Technical Approach: ------ - -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 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). - -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. - -Some useful resources: -**Ruby Object Mapper** -- [ROM](https://rom-rb.org/) - -**ActiveRecord** -- [ActiveRecord ORM](https://guides.rubyonrails.org/active_record_basics.html) -- [Sinatra & ActiveRecord setup](https://learn.co/lessons/sinatra-activerecord-setup) - -Notes on functionality: ------- - -* 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. - -Bonus: ------ - -If you have time you can implement the following: - -* In order to start a conversation as a maker I want to reply to a peep from another maker. - -And/Or: - -* Work on the CSS to make it look good. - -Good luck and let the chitter begin! - -Code Review ------------ - -In code review we'll be hoping to see: - -* 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. - -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. - -Notes on test coverage ----------------------- - -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: - -```ruby -require 'simplecov' -require 'simplecov-console' - -SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::Console, - # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter -]) -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! diff --git a/Tables Design Template.md b/Tables Design Template.md new file mode 100644 index 0000000000..3fce24a927 --- /dev/null +++ b/Tables Design Template.md @@ -0,0 +1,157 @@ +# Two Tables Design Recipe Template + +_Copy this recipe template to design and create two related database tables from a specification._ + +## 1. Extract nouns from the user stories or specification + +``` +STRAIGHT UP + +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 + +HARDER + +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 + +ADVANCED + +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 +``` +Nouns: + +posts, message, time, signup, login, logout +``` + +## 2. Infer the Table Name and Columns + +Put the different nouns in this table. Replace the example with your own nouns. + +| Record | Properties | +| --------------------- | ------------------ | +| Users | user_id, username, email, password +| Posts | posts_id, timestamp, message + +1. Name of the first table (always plural): `users` + + Column names: `user_id, username, email, password' + +2. Name of the second table (always plural): `posts + + Column names: `posts_id`, `timestamp`, 'message' + +## 3. Decide the column types. + +[Here's a full documentation of PostgreSQL data types](https://www.postgresql.org/docs/current/datatype.html). + +Most of the time, you'll need either `text`, `int`, `bigint`, `numeric`, or `boolean`. If you're in doubt, do some research or ask your peers. + +Remember to **always** have the primary key `id` as a first column. Its type will always be `SERIAL`. + +``` +# EXAMPLE: + +Table: users +id: SERIAL +username: text +email: text +password: text + + +Table: posts +id: SERIAL +timestamp: timestasmp +message: text +``` + +## 4. Decide on The Tables Relationship + +Most of the time, you'll be using a **one-to-many** relationship, and will need a **foreign key** on one of the two tables. + +To decide on which one, answer these two questions: + +1. Can one [TABLE ONE] have many [TABLE TWO]? (yes) +2. Can one [TABLE TWO] have many [TABLE ONE]? (no) + +You'll then be able to say that: + +1. **[A] has many [B]** +2. And on the other side, **[B] belongs to [A]** +3. In that case, the foreign key is in the table [B] + +Replace the relevant bits in this example with your own: + +``` +Therefore the foreign key is on comments (post_id) + +``` + +*If you can answer YES to the two questions, you'll probably have to implement a Many-to-Many relationship, which is more complex and needs a third table (called a join table).* + +## 4. Write the SQL. + +```sql +-- EXAMPLE +-- file: albums_table.sql + +-- Replace the table name, columm names and types. + +-- Create the table without the foreign key first. +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username text, + email text, + password text +); + +-- Then the table with the foreign key first. +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + timestamp timestamp, + message text +-- The foreign key name is always {other_table_singular}_id + user_id int, + constraint fk_post foreign key(user_id) + references users(id) + on delete cascade +); + +``` + +## 5. Create the tables. + +```bash +psql -h 127.0.0.1 database_name < albums_table.sql +``` + + + + +--- + +**How was this resource?** +[😫](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=😫) [😕](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=😕) [😐](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=😐) [🙂](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=🙂) [😀](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=😀) +Click an emoji to tell us. + + \ No newline at end of file diff --git a/app.rb b/app.rb new file mode 100644 index 0000000000..7a7718c74a --- /dev/null +++ b/app.rb @@ -0,0 +1,106 @@ +require 'sinatra' +require 'sinatra/reloader' +require 'active_record' +require_relative 'lib/database_connection' +require_relative 'lib/post' +require_relative 'lib/user' + +# Establish the database connection +establish_database_connection +set :public_folder, File.dirname(__FILE__) + '/public' + +class Application < Sinatra::Base + enable :sessions + configure :development do + register Sinatra::Reloader + also_reload 'lib/database_connection' + also_reload 'lib/post' + also_reload 'lib/user' + end + + configure do + # Set the default timezone to GMT + ENV['TZ'] = 'GMT' + @@posts = Post.all_peeps.reverse + end + + get '/' do + erb(:index) + end + + post '/account_page' do + current_time = Time.now + 1 * 60 * 60 + formatted_time = current_time.strftime("%d/%m/%Y %H:%M") + Post.create_post(formatted_time, params[:message], session[:user_id]) + + + @@posts.unshift("#{formatted_time} #{User.find(session[:user_id]).username} #{params[:message]}") + + redirect '/account_page' + end + + + get '/signup' do + erb(:signup) + end + + get '/login' do + erb(:login) + end + + get '/logout' do + session.clear + redirect '/' + end + + get '/login_failure' do + erb(:login_failure) + end + + post '/login' do + user = User.sign_in(params[:username], params[:password]) + if user + session[:user_id] = user.id + redirect '/account_page' + else + redirect '/login_failure' + end + end + + + + get '/account_page' do + if session[:user_id].nil? + # No user id in the session + # so the user is not logged in. + redirect '/login' + else + # The user is logged in, display their account page. + erb(:account) + end + end + + + post '/signup' do + database = User.all_records + match_found = false + + database.each do |record| + if record.username == params[:username] || record.email == params[:email] + match_found = true + break + end + end + + if match_found + redirect '/signup' + else + User.create_user(params[:name], params[:username], params[:email], params[:password]) + redirect '/success' + end + end + + get '/success' do + erb(:success) + end +end \ No newline at end of file diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..c41dba5056 --- /dev/null +++ b/config.ru @@ -0,0 +1,3 @@ +# file: config.ru +require './app' +run Application diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 0000000000..c2569bdabb --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,11 @@ +# file: lib/database_connection.rb +require 'active_record' + +def establish_database_connection + ActiveRecord::Base.establish_connection( + adapter: 'postgresql', + host: 'localhost', + port: '5432', + database: 'chitter_test', + ) +end diff --git a/lib/post.rb b/lib/post.rb new file mode 100644 index 0000000000..2634cc4742 --- /dev/null +++ b/lib/post.rb @@ -0,0 +1,21 @@ +require 'active_record' +require_relative './database_connection' +establish_database_connection + +class Post < ActiveRecord::Base + belongs_to :user + @posts = [] + def self.all_peeps + Post.joins(:user).map do |post| + @posts << "#{post.time} #{post.user.name} #{post.user.username} #{post.message}" + end + @posts + end + + def self.create_post(time, message, user_id) + post = Post.new(time: time, message: message, user_id: user_id) + post.save + post + end + +end \ No newline at end of file diff --git a/lib/user.rb b/lib/user.rb new file mode 100644 index 0000000000..39f1f73f95 --- /dev/null +++ b/lib/user.rb @@ -0,0 +1,33 @@ +require 'active_record' +require_relative 'database_connection' +require 'bcrypt' +establish_database_connection + +class User < ActiveRecord::Base + has_many :posts + def self.all_records + @users = [] + User.all.map do |user| + @users << user + end + @users + end + + def self.create_user(name, username, email, password) + encrypted_password = BCrypt::Password.create(password) + user = User.new(name: name, username: username, email: email, password: encrypted_password) + user.save + user + end + + def self.sign_in(username, password) + user = User.find_by(username: username) + return nil unless user + + stored_password = BCrypt::Password.new(user.password) + return nil unless stored_password == password + + user + end +end + \ No newline at end of file diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000000..352a714201 Binary files /dev/null and b/public/icon.png differ diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000000..65b176a69a --- /dev/null +++ b/public/style.css @@ -0,0 +1,412 @@ +html { + box-sizing: border-box; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +body { + font-family: sans-serif; + border: 0; + padding: 0; + margin: 0; + background-color: rgb(255, 255, 255); +} + +header { + background-color: rgb(41, 94, 163); + width: 100%; + height: 150px; + display: flex; + align-items: center; + justify-content: center; + color: white; + padding: 0 20px; +} + +.icon-text-container { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + position: relative; +} + +.icon { + width: 100px; + height: 100px; + margin-bottom: 10px; /* Create spacing between icon and text */ +} + +.text { + font: 3rem "Roboto", sans-serif; + /* No need for absolute positioning or align-self */ +} + +.buttons-container { + position: absolute; + top: 90px; + right: 80px; /* Adjust this value to position the buttons */ + display: flex; + align-items: center; + justify-content: space-around; +} + +.header-button { + width: 150px; + height: 40px; + background-color: rgb(255, 255, 255); + border: none; + color: rgb(41, 94, 163); + border-radius: 6px; + font-size: 15px; + text-align: center; + transition-duration: 0.2s; + font-weight: 600; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + margin-top: 5px; + margin-right: 5px; +} + +.header-button:hover { + background-color: rgb(255, 221, 0); + cursor: pointer; +} + +#container { + align-items: center; + display: flex; + flex-flow: column; + width: 100%; + background-color: white; + min-height: calc(100vh - 150px); + padding: 15px; +} + +.post { + border: 1px solid rgb(41, 94, 163); + display: flex; + justify-content: space-between; + gap: 50px; + margin: 20px; + width: 100%; + max-width: 600px; /* Set the maximum width */ + height: auto; /* Adjust the height as needed */ + padding: 20px; /* Add padding for spacing inside the box */ +} + +#input-form { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + gap: 20px; + width: 100%; + max-width: 600px; /* Set the maximum width */ + margin-top: 50px; +} + +#input-form .username, +#input-form .password { + width: 100%; + height: 45px; /* Adjust the height as needed */ + padding: 8px; /* Adjust the padding as needed */ + font-size: 16px; /* Adjust the font size as needed */ + border: 1px solid rgb(41, 94, 163); + border-radius: 6px; +} + +.login-button { + width: 150px; + height: 40px; + background-color: rgb(255, 255, 255); + border: none; + color: rgb(41, 94, 163); + border-radius: 6px; + font-size: 15px; + text-align: center; + transition-duration: 0.2s; + font-weight: 600; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + margin-left: 115px; + margin-top: 30px; +} + +.login-button:hover { + background-color: rgb(255, 221, 0); + cursor: pointer; +} + +.post-button { + width: 150px; + height: 40px; + background-color: rgb(255, 255, 255); + border-style: solid; + color: rgb(41, 94, 163); + border-radius: 6px; + font-size: 15px; + text-align: center; + transition-duration: 0.2s; + font-weight: 600; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + margin-left: 230px; + margin-top: 30px; +} + +.post-button:hover { + background-color: rgb(255, 221, 0); + cursor: pointer; +} + +.input-post-box { + width: 600px; + height: auto; /* Adjust the height as needed */ + padding: 8px; /* Adjust the padding as needed */ + font-size: 16px; /* Adjust the font size as needed */ + border: 1px solid rgb(41, 94, 163); + border-radius: 6px; + resize: vertical; + margin-top: 15px; +} + +#location-list a { + text-decoration: none; + color: white; +} + +figure { + display: flex; + flex-flow: column nowrap; + position: relative; +} +figcaption { + position: absolute; + bottom: 0%; + width: 100%; + border-bottom-right-radius: 10px; + border-bottom-left-radius: 10px; +} + +/* Basic figure and caption styling */ +/* Turn off margin, set by browser */ +figure { + margin: 0; + padding: 0; + transition: transform 0.2s; +} +/* caption styling */ +figcaption { + background-color: rgba(255, 255, 255, 0.8); + color: rgba(73, 143, 133, 1); + padding: 0.5rem; + font-size: 0.7rem; +} +/* reduces the top margin on the h2, bottom on the p, and leaves a 0.5rem gap between the two */ +figcaption * { + margin: 0.5rem; +} + +figcaption p { + font-weight: bold; + font-size: 0.8rem; +} +/* hack to make images flexible */ +img { + width: 100%; +} + +figure:hover { + transform: scale(1.04); +} + +#location-list .location-card { + max-width: 100%; + position: relative; +} + +#location-list .location-photo { + border-radius: 10px; + width: 90vw; + height: 250px; + object-fit: cover; + opacity: 1; +} + +#location-details { + max-width: 600px; +} + +#location-details .location-description { + margin-top: 0px; + background-color: rgba(73, 143, 133, 0.3); + padding: 15px; + line-height: 1.2rem; + color: rgb(80, 80, 80); + font-size: 0.95rem; + border-radius: 10px; +} + +#location-details > img { + border-radius: 10px; + margin-top: 2rem; +} + +#location-details > h4 { + margin: 0; +} + +#location-details .audio-player { + margin-top: 20px; + align-self: center; +} + +#location-details .speech-button { + background: transparent; + border: 3px solid rgba(73, 143, 133, 0.5); + border-radius: 5px; + outline: "none"; +} + +.logo img { + width: 250px; +} + +.logo-container { + width: 100%; + height: 100%; +} + +.loading-container, +.greeting-screen { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + margin: auto; + width: 70vw; + height: 70vh; + max-width: 500px; + max-height: 500px; +} + +/* drop down box in locations */ +.type-dropdown { + padding: 10px 15px; + font-size: 16px; + border: none; + border-radius: 4px; + color: #fff; + appearance: none; + outline: none; + margin-top: 30px; + margin-bottom: 20px; + background-image: url(/dropdown-filter.png); + width: 200px; +} + +.type-dropdown:hover { + transform: scale(1.03); +} + +.type-dropdown:hover { + cursor: pointer; +} + +#start-button, +button { + width: 150px; + height: 40px; + background-color: rgba(73, 143, 133, 1); + border: none; + color: white; + border-radius: 6px; + font-size: 15px; + transition-duration: 0.2s; + font-weight: 600; + text-decoration: none; +} + +#start-button:hover, +button:hover { + background-color: rgba(73, 143, 133, 0.9); + cursor: pointer; +} + +/* css for more option button in location */ + +.dropdown { + display: flex; + overflow: hidden; + width: 0; + transition: width 1s ease-in-out; +} + +.dropdown-item { + transform: translateX(-100%); + animation: roll-out 1s forwards; +} + +@keyframes roll-out { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +/* ratings css */ + +.rating-container { + display: flex; + align-items: center; +} + +.rating-icon { + color: #f8e825; + margin-right: 0.5rem; +} + +.rating { + font-size: 1rem; + font-weight: bold; +} + +@media (min-width: 600px) { + .loading-container, + .greeting-screen { + width: 100vw; + height: 100vh; + } + + #location-list .location-photo { + width: 550px; + height: 275px; + } + + #location-details .location-description { + padding: 30px; + font-size: 1rem; + line-height: 1.35rem; + } + + .startpage-wrapper { + gap: 2.5rem; + } +} diff --git a/screenshot1.png b/screenshot1.png new file mode 100644 index 0000000000..c32b66d379 Binary files /dev/null and b/screenshot1.png differ diff --git a/screenshot2.png b/screenshot2.png new file mode 100644 index 0000000000..ffe3495a9a Binary files /dev/null and b/screenshot2.png differ diff --git a/spec/integration/application.spec.rb b/spec/integration/application.spec.rb new file mode 100644 index 0000000000..0043dca268 --- /dev/null +++ b/spec/integration/application.spec.rb @@ -0,0 +1,88 @@ +require "spec_helper" +require "rack/test" +require_relative '../../app' +require 'reset_tables' +require 'user' +require 'post' + +describe Application do + # This is so we can use rack-test helper methods. + include Rack::Test::Methods + + # We need to declare the `app` value by instantiating the Application + # class so our tests work. + let(:app) { Application.new } + before(:each) do + reset = ResetTables.new + reset.reset_posts_table + reset.reset_users_table + end + + context 'Get /' do + it 'returns HTML page' do + response = get('/') + expect(response.status).to eq(200) + expect(response.body).to include('Chitter') + end + end + context 'Get /signup' do + it 'returns signup page' do + response = get('/signup') + expect(response.status).to eq(200) + expect(response.body).to include('username') + end + end + context 'Get /sucess' do + it 'returns successpage' do + response = get('/success') + expect(response.status).to eq(200) + expect(response.body).to include('Success') + end + end + context 'Post /account_page' do + it 'adds peep to chitter' do + post '/login', username: 'test_user', password: 'password' # Login the user and set the session + + post '/account_page', { message: 'Need to start working' }, 'rack.session' => { user_id: 2 } + + follow_redirect! # Follow the redirect to the new page + expect(last_request.path).to eq('/account_page') # Ensure we are on the expected page + expect(last_response.body).to include('Need to start working') # Check the content of the page + + expect(last_response.status).to eq(200) # Optionally check the status code + end + end + + context 'Post /signup' do + it 'recognises that a username already exists, redirects to the signup page' do + user = User.new + response = post('/signup', { name: 'Need to start working', username: 'laurenhannis', email: 'jdjdhjsjksd', password: 'jdsjdnsjs' }) + User.create_user('Lauren Hannis', 'laurenhannis', 'jdjdhjsjksd', 'jdsjdnsjs') + + expect(response.status).to eq(302) # Assuming it redirects + expect(response.headers['Location']).to include('/signup') + end + end + context 'Post /login' do + it 'logs into user account' do + response = post('/login', { username: 'laurenhannis', password: 'passwordlauren' }) + User.sign_in('laurenhannis', 'passwordlauren') + + expect(response.status).to eq(302) # Assuming it redirects + expect(response.headers['Location']).to include('/account_page') + end + end + context 'GET /logout' + it 'clears the session and redirects to the home page' do + post '/login', { username: 'testuser', password: 'password' } + + get '/logout' + + expect(last_response.redirect?).to be true + follow_redirect! + + expect(last_request.path).to eq('/') + expect(rack_mock_session.cookie_jar['rack.session']).not_to include('user_id') + end +end + \ No newline at end of file diff --git a/spec/post_spec.rb b/spec/post_spec.rb new file mode 100644 index 0000000000..be40b98b0c --- /dev/null +++ b/spec/post_spec.rb @@ -0,0 +1,18 @@ +require 'post' +require 'reset_tables' + +RSpec.describe Post do + before(:each) do + reset = ResetTables.new + reset.reset_posts_table + end + + it 'return all posts from the database' do + expect(Post.all_peeps[0]).to include("Hey howre you doing") + end + + it 'creates a new post and returns the table' do + Post.create_post(Time.now, 'Its Friday', 2) + expect(Post.all_peeps[-1]).to include("Its Friday") + end +end diff --git a/spec/reset_tables.rb b/spec/reset_tables.rb new file mode 100644 index 0000000000..a7c1fd91db --- /dev/null +++ b/spec/reset_tables.rb @@ -0,0 +1,13 @@ +class ResetTables + def reset_posts_table + seed_sql = File.read('spec/seeds/post_seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' }) + connection.exec(seed_sql) + end + + def reset_users_table + seed_sql = File.read('spec/seeds/user_seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' }) + connection.exec(seed_sql) + end +end diff --git a/spec/seeds/post_seeds.sql b/spec/seeds/post_seeds.sql new file mode 100644 index 0000000000..d285477e04 --- /dev/null +++ b/spec/seeds/post_seeds.sql @@ -0,0 +1,17 @@ +DROP TABLE IF EXISTS posts CASCADE; + +-- Table Definition +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + time TIMESTAMP, + message text, + user_id int +); + +TRUNCATE TABLE posts RESTART IDENTITY; + +INSERT INTO posts ("time", "message", "user_id") VALUES + (CURRENT_TIMESTAMP, 'Hey howre you doing', 1), + (CURRENT_TIMESTAMP, 'Warm out today', 2), + (CURRENT_TIMESTAMP, 'Warm out yesterday', 2), + (CURRENT_TIMESTAMP, 'Coffefe', 3); diff --git a/spec/seeds/user_seeds.sql b/spec/seeds/user_seeds.sql new file mode 100644 index 0000000000..4564ab79d1 --- /dev/null +++ b/spec/seeds/user_seeds.sql @@ -0,0 +1,31 @@ +SET client_min_messages = WARNING; +CREATE EXTENSION IF NOT EXISTS pgcrypto; +-- Define the table structure +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name text, + username text, + email text, + password text +); + +-- Clear existing data and reset the auto-incrementing ID +TRUNCATE TABLE users RESTART IDENTITY; + +-- Helper function to encrypt passwords using BCrypt +CREATE OR REPLACE FUNCTION encrypt_password(password text) + RETURNS text AS $$ + DECLARE + hashed_password text; + BEGIN + hashed_password := crypt(password, gen_salt('bf')); + RETURN hashed_password; + END; +$$ LANGUAGE plpgsql; + +-- Insert user records with encrypted passwords +INSERT INTO users ("name", "username", "email", "password") VALUES +('Joe Hannis', 'joehannis', 'joehannis@gmail.com', encrypt_password('passwordjoe')), +('Jake Hannis', 'jakehannis', 'jakehannis@gmail.com', encrypt_password('passwordjake')), +('Lauren Hannis', 'laurenhannis', 'laurenhannis@gmail.com', encrypt_password('passwordlauren')), +('Luna Hannis', 'lunahannis', 'lunahannis@gmail.com', encrypt_password('passwordluna')); \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d899..3983c9ba73 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,8 @@ require 'simplecov' require 'simplecov-console' +require 'database_connection' + +establish_database_connection SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console, @@ -9,6 +12,84 @@ SimpleCov.start RSpec.configure do |config| + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end config.after(:suite) do puts puts "\e[33mHave you considered running rubocop? It will help you improve your code!\e[0m" diff --git a/spec/user_spec.rb b/spec/user_spec.rb new file mode 100644 index 0000000000..b50bf017d1 --- /dev/null +++ b/spec/user_spec.rb @@ -0,0 +1,18 @@ +require 'user' +require 'reset_tables' + +RSpec.describe User do + before(:each) do + reset = ResetTables.new + reset.reset_users_table + end + it 'return all users from the database' do + expect(User.all_records[0][:email]).to eq('joehannis@gmail.com') + expect(User.all_records[1][:email]).to eq('jakehannis@gmail.com') + end + it 'creates a new user and returns the table' do + User.create_user('John Smith', 'johnsmith', 'john@example.com', 'password123') + expect(User.all_records[4][:email]).to eq('john@example.com') + end + +end diff --git a/views/account.erb b/views/account.erb new file mode 100644 index 0000000000..538cda5fe7 --- /dev/null +++ b/views/account.erb @@ -0,0 +1,32 @@ + + +
+