Fetch:Login/Logout, getTweets, deleteTweet

This commit is contained in:
da3491 2022-11-28 12:00:45 -05:00
parent 3f61aae359
commit 634e59883a
23 changed files with 614 additions and 125 deletions

View File

@ -27,7 +27,7 @@ gem 'jbuilder', '~> 2.11'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1'
gem 'bcrypt', '~> 3.1'
# Use Active Storage variant
# gem 'image_processing', '~> 1.2'

View File

@ -78,6 +78,7 @@ GEM
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.1)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.18)
bindex (0.8.1)
bootsnap (1.13.0)
msgpack (~> 1.2)
@ -279,6 +280,7 @@ PLATFORMS
DEPENDENCIES
awesome_print (~> 1.9)
aws-sdk-s3 (~> 1.114)
bcrypt (~> 3.1)
bootsnap (>= 1.13)
byebug (~> 11.1.3)
dotenv-rails (~> 2.8)

View File

@ -1,5 +1,5 @@
module Api
class SessionsController < ApplicationController
class SessionsController < ApplicationController
def create
@user = User.find_by(username: params[:user][:username])

View File

@ -1,4 +1,13 @@
class StaticPagesController < ApplicationController
def home
render 'home'
end
def login
render 'login'
end
def user
render 'user'
end
end

View File

@ -0,0 +1 @@
import '@src/login'

View File

@ -1 +0,0 @@
import '@src/tweets'

View File

@ -0,0 +1 @@
import '@src/user'

View File

@ -1,69 +1,211 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { handleErrors, safeCredentials } from '../utils/fetchHelper';
import Tweet from './tweet'
import './home.scss';
const Home = () => (
<>
<nav className='navbar navbar-expand navbar-light bg-light'>
<div className='container'>
<a className='navbar-brand' href="#">Logo</a>
<div className='collapse navbar-collapse'>
<label htmlFor='language'>language:</label>
<select name="language" id='language_dropdown'>
<option>Bahasa Malaya</option>
<option>Dansk</option>
<option>English</option>
<option>Suomi</option>
</select>
</div>
</div>
</nav>
<main className='container row m-auto mt-5'>
<div className='col-7 d-flex flex-column justify-content-between'>
<div className='me-lg-5'>
<h1 className='mb-4'>Welcome to Twitter.</h1>
<p>Connect with your friends - and other fascinating people. Get in-the-moment updates on the things that interest you. And watch events unfold, in real time, from every angle.</p>
</div>
<div>
<div>Hack Pacific - Backendium Twitter Project</div>
<a>Tweet and photo by @Hackpacific 3:20PM - 15 December 2016</a>
</div>
</div>
<div className='col-5'>
<div id="section__id" className='border rounded mb-3'>
<div className='d-flex flex-column gap-3 m-3'>
<input className='w-100' placeholder='Username'></input>
<div className='d-flex justify-content-between'>
<input className='flex-shrink-1 ' placeholder='Password'></input>
<button className='btn btn-primary btn-sm text-nowrap'>Log in</button>
</div>
<div className='d-flex'>
<div className='d-flex'>
<input type='checkbox' className='me-2' />
<div className='me-2'>Remember Me</div>
// create class component
// componentDidMount-fetch tweets
// logout => delete session
// get tweets
// post tweet
// delete tweet
// get tweets/:user
class Home extends React.Component {
state = {
tweets: [],
username: '',
message: '',
loading: true,
error: ''
}
componentDidMount() {
this.getTweets()
this.getUser()
}
handleChange = (e) => {
e.preventDefault()
this.setState({
message: e.target.value
})
}
getTweets = (e) => {
if (e) { e.preventDefault(); }
this.setState({
error: '',
});
fetch('/api/tweets', safeCredentials({
method: 'GET',
}))
.then(handleErrors)
.then(data => {
this.setState({
tweets: data.tweets,
loading: false,
})
})
.catch(error => {
this.setState({ error: 'Could not get tweets.' })
})
}
getUser = (e) => {
if (e) { e.preventDefault(); }
this.setState({
error: '',
});
fetch('/api/authenticated', safeCredentials({
method: 'GET',
}))
.then(handleErrors)
.then(data => {
this.setState({
username: data.username,
loading: false,
})
})
.catch(error => {
this.setState({ error: 'Could not get user.' })
})
}
endSession = (e) => {
if (e) { e.preventDefault(); }
this.setState({
error: '',
});
fetch('/api/sessions', safeCredentials({
method: 'DELETE',
}))
.then(handleErrors)
.then(data => {
if (data.success) {
console.log('successfully ended session')
const params = new URLSearchParams(window.location.search);
const redirect_url = params.get('redirect_url') || '/login';
window.location = redirect_url;
}
}).catch(error => {
this.setState({ error: 'Could not log out.' })
})
}
createTweet = (e) => {
if (e) { e.preventDefault(); }
this.setState({
error: '',
});
fetch('/api/tweets', safeCredentials({
method: 'POST',
body: {
username: this.state.username,
message: this.state.message,
}
}))
.then(handleErrors)
.then(data => {
console.log(data)
})
.catch(error => {
this.setState({ error: 'Could not post tweet.' })
})
}
render() {
const { tweets, username, loading } = this.state
return (
<>
<nav className='navbar navbar-expand navbar-light bg-light'>
<div className='container'>
<a href='#'>
<i className="fa-brands fa-twitter fs-5 text-primary"></i>
</a>
<div className='collapse navbar-collapse'>
<div className='input-group ms-auto'>
<input type='text' className='form-control' placeholder='Search for...' />
<span className='input-group-text'>Go!</span>
</div>
<div className='text-secondary ms-5'>
<span role='button' className='text-decoration-none' onClick={this.endSession}>logout</span>
</div>
<a href="#" className='text-decoration-none'>Forgot password?</a>
</div>
</div>
</div>
<div id='section__signup' className='border rounded p-3'>
<div>
<strong className='pe-2'>New to Twitter?</strong>
<a href="#" className='text-decoration-none text-secondary'>Sign up</a>
</nav>
<main className='container row gap-3 justify-content-center'>
<div className='col-4 my-3'>
<div className='border p-3 bg-white rounded'>
<div>
<h4 className='mb-0'>{username}</h4>
<p className='text-secondary'>@{username}</p>
</div>
<div className='row'>
<div className='col-4'>
<span className='text-secondary'>Tweets</span>
<div className='text-primary'>4</div>
</div>
<div className='col-4'>
<span className='text-secondary'>Following</span>
<div className='text-primary'>0</div>
</div> <div className='col-4'>
<span className='text-secondary'>Followers</span>
<div className='text-primary'>0</div>
</div>
</div>
</div>
<div className='col border my-3 bg-white rounded'>
<div className='p-3'>
<div className='d-flex align-items-center'>
<div className='fs-4 text-secondary'>Trends</div>
<div className='mx-2 mb-1'>.</div>
<div className='text-primary'>Change</div>
</div>
<ul className='text-decoration-none list-unstyled'>
<li className='text-primary'>#<a>Hongkong</a></li>
<li className='text-primary'>#<a>Ruby</a></li>
<li className='text-primary'>#<a>foobarbaz</a></li>
<li className='text-primary'>#<a>rails</a></li>
<li className='text-primary'>#<a>API</a></li>
</ul>
</div>
</div>
</div>
<div className='input-group'>
<input className='my-2 w-100 form-control' placeholder='Username'></input>
<input className='my-2 w-100 form-control' placeholder="Email"></input>
<input className='my-2 w-100 form-control' placeholder='Password'></input>
<div className='col-6 p-0 my-3 rounded'>
<form id='post-tweet' className='p-3' onSubmit={this.createTweet}>
<input type="text"
className='w-100 mb-3 form-control'
placeholder="What's happening?"
onChange={this.handleChange} />
<div className='d-flex align-items-center justify-content-end'>
<div className='pe-3'>140</div>
<button type='submit' className='btn btn-sm btn-primary'>Tweet</button>
</div>
</form>
<div id='tweet-feed'>
{tweets.map(tweet => {
return <Tweet key={tweet.id} props={tweet} />
})}
</div>
</div>
<button className='btn btn-warning fw-bold'>Sign up for Twitter</button>
</div>
<div></div>
</div>
</main>
</>
)
<div></div>
</main>
</>
)
}
}
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(

View File

@ -0,0 +1,15 @@
body {
background-color: lightskyblue;
}
nav .input-group {
width: 250px;
}
#post-tweet {
background-color: rgb(91, 176, 230);
}
#post-tweet input {
height: 100px;
}

View File

@ -0,0 +1,78 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { safeCredentials, handleErrors } from '../utils/fetchHelper';
import LoginWidget from './loginWidget';
import SignupWidget from './signupWidget';
import './login.scss';
// post user
// get user/authenticated
class Login extends React.Component {
state = {
authenticated: false,
show_login: true,
}
componentDidMount() {
fetch('/api/authenticated')
.then(handleErrors)
.then(data => {
this.setState({
authenticated: data.authenticated,
})
})
}
render() {
const { authenticated, show_login } = this.state;
return (
<>
<nav className='navbar navbar-expand navbar-light bg-light'>
<div className='container'>
<a href="#">
<i className="fa-brands fa-twitter fs-5 text-primary"></i>
</a>
<div className='collapse navbar-collapse'>
<div className='ms-auto'>
<label htmlFor='language'>language:</label>
<select className='ms-2' name="language" id='language_dropdown'>
<option>Bahasa Malaya</option>
<option>Dansk</option>
<option>English</option>
<option>Suomi</option>
</select>
</div>
</div>
</div>
</nav>
<main className='container row m-auto mt-5'>
<div className='col-6 d-flex flex-column justify-content-between'>
<div className='me-lg-5'>
<h1 className='mb-4'>Welcome to Twitter.</h1>
<p>Connect with your friends - and other fascinating people. Get in-the-moment updates on the things that interest you. And watch events unfold, in real time, from every angle.</p>
</div>
<div>
<div>Hack Pacific - Backendium Twitter Project</div>
<a>Tweet and photo by @Hackpacific 3:20PM - 15 December 2016</a>
</div>
</div>
<div className='col-4'>
<LoginWidget />
<SignupWidget />
</div>
</main>
</>
)
}
}
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(
<Login />,
document.body.appendChild(document.createElement('div')),
)
})

View File

@ -0,0 +1,13 @@
body {
width: 100vw;
height: 100vh;
background-image: url('https://raw.githubusercontent.com/Altcademy/bewd-twitter/main/app/assets/images/background_1.png');
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
#signup__button,
#remember__me {
font-size: .7rem;
}

View File

@ -0,0 +1,81 @@
import React from 'react'
import { handleErrors, safeCredentials } from '../utils/fetchHelper'
class LoginWidget extends React.Component {
state = {
username: '',
password: '',
error: '',
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value,
})
}
login = (e) => {
if (e) { e.preventDefault(); }
this.setState({
error: '',
});
fetch('/api/sessions', safeCredentials({
method: 'POST',
body: JSON.stringify({
user: {
username: this.state.username,
password: this.state.password,
}
})
}))
.then(handleErrors)
.then(data => {
if (data.success) {
const params = new URLSearchParams(window.location.search);
const redirect_url = params.get('redirect_url') || '/';
window.location = redirect_url;
}
}).catch(error => {
this.setState({
error: 'Could not log in.',
})
})
}
render() {
const { username, password } = this.state
return (
<div id="section__login" className='border rounded mb-3 bg-white'>
<form className='d-flex flex-column gap-3 m-3' onSubmit={this.login}>
<input name="username"
type="text"
className='w-100 form-control'
placeholder='Username'
value={username}
onChange={this.handleChange}
required></input>
<div className='form-group d-flex mw-100'>
<input name="password"
className='form-control'
placeholder='Password'
value={password}
onChange={this.handleChange}
required></input>
<button type="submit" className='btn btn-primary btn-sm text-nowrap ms-3'>Log in</button>
</div>
<div id='remember__me' className='d-flex'>
<label className='d-flex'>
<input type='checkbox' className='me-2' />
<span className='me-2'>Remember Me</span>
</label>
<a href="#" className='text-decoration-none'>Forgot password?</a>
</div>
</form>
</div>
)
}
}
export default LoginWidget;

View File

@ -0,0 +1,100 @@
import React from 'react'
import { safeCredentials, handleErrors } from '../utils/fetchHelper';
class SignupWidget extends React.Component {
state = {
username: '',
email: '',
password: '',
error: '',
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value,
})
}
signup = (e) => {
if (e) { e.preventDefault(); }
this.setState({
error: '',
});
console.log('clicked')
fetch('/api/users', safeCredentials({
method: 'POST',
body: JSON.stringify({
user: {
email: this.state.email,
username: this.state.username,
password: this.state.password,
}
})
}))
.then(handleErrors)
.then(data => {
console.log(success)
if (data.user) {
this.login()
}
}).catch(error => {
this.setState({
error: 'Could not sign up.',
})
})
}
login = (e) => {
if (e) { e.preventDefault(); }
this.setState({
error: '',
});
fetch('/api/sessions', safeCredentials({
method: 'POST',
body: JSON.stringify({
user: {
username: this.state.username,
password: this.state.password,
}
})
}))
.then(handleErrors)
.then(data => {
if (data.success) {
console.log(success)
const params = new URLSearchParams(window.location.search);
const redirect_url = params.get('redirect_url') || '/';
window.location = redirect_url;
}
}).catch(error => {
this.setState({
error: 'Could not log in.',
})
})
}
render() {
const { email, username, password, error } = this.state
return (
<form id='section__signup' className='border rounded p-3 bg-white' onSubmit={this.signup}>
<div>
<strong className='pe-2'>New to Twitter?</strong>
<a href="#" className='text-decoration-none text-secondary'>Sign up</a>
</div>
<div className='input-group'>
<input name="username" className='my-2 w-100 form-control' placeholder='Username' value={username} onChange={this.handleChange}></input>
<input name="email" className='my-2 w-100 form-control' placeholder="Email" value={email} onChange={this.handleChange}></input>
<input name="password" className='my-2 w-100 form-control' placeholder='Password' value={password} onChange={this.handleChange}></input>
</div>
<div className='d-flex'>
<button id='signup__button' type="submit" className='btn btn-warning fw-bold ms-auto'>Sign up for Twitter</button>
</div>
</form>
)
}
}
export default SignupWidget;

View File

@ -0,0 +1,37 @@
import React from 'react'
import { handleErrors, safeCredentials } from '../utils/fetchHelper';
const Tweet = ({ props }) => {
const deleteTweet = (e) => {
if (e) { e.preventDefault(); }
fetch(`/api/tweets/${props.id}`, safeCredentials({
method: 'DELETE',
}))
.then(handleErrors)
.then(data => {
console.log(data)
})
.catch(error => {
console.log(error)
})
}
return (
<>
<div className='col border-top border-bottom m-0 bg-white'>
<div className='p-3'>
<div className='d-flex align-items-start align-text-center'>
<div className='fw-bold me-2'>{props.username}</div>
<a className='text-secondary text-decoration-none' href='/user'>@{props.username}</a>
</div>
<p>{props.message}</p>
<div className='d-flex'>
<button className='btn border-none m-0 p-0 ms-auto text-primary' onClick={deleteTweet}>Delete</button>
</div>
</div>
</div>
</>
)
}
export default Tweet;

View File

@ -1,65 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom';
const Tweets = () => {
return (
<>
<nav>
<div>logo</div>
<div>
<input placeholder='Search for...'></input>
<button>Go!</button>
<div>username</div>
</div>
</nav>
<div id='section__userstats'>
<h1>username</h1>
<div>@username</div>
<div>
<div>
<div>TWEETS</div>
<div>0</div>
</div>
<div>
<div>FOLLOWING</div>
<div>0</div>
</div>
<div>
<div>FOLLOWERS</div>
<div>0</div>
</div>
</div>
</div>
<div id='section__trending'>
<div>
<h2>Trends</h2>
<a href="#">change</a>
</div>
<div href="#">#HongKong</div>
<div href="#">#Ruby</div>
<div href="#">#foobarbaz</div>
<div href="#">#rails</div>
<div href="#">#API</div>
</div>
<form>
<input placeholder="What's happening?">
</input>
<div>
<div>Upload image</div>
<div>140</div>
<button>Tweet</button>
</div>
</form>
<div>
<div>Feed goes here</div>
</div>
</>
)
}
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(
<Tweets />,
document.body.appendChild(document.createElement('div')),
)
})

View File

@ -0,0 +1,9 @@
import React from 'react'
const User = () => {
return (
<h1>User page</h1>
)
}
export default User;

View File

@ -0,0 +1,46 @@
/**
* For use with window.fetch
*/
export function jsonHeader(options = {}) {
return Object.assign(options, {
Accept: 'application/json',
'Content-Type': 'application/json',
});
}
// Additional helper methods
export function getMetaContent(name) {
const header = document.querySelector(`meta[name="${name}"]`);
return header && header.content;
}
export function getAuthenticityToken() {
return getMetaContent('csrf-token');
}
export function authenticityHeader(options = {}) {
return Object.assign(options, {
'X-CSRF-Token': getAuthenticityToken(),
'X-Requested-With': 'XMLHttpRequest',
});
}
/**
* Lets fetch include credentials in the request. This includes cookies and other possibly sensitive data.
* Note: Never use for requests across (untrusted) domains.
*/
export function safeCredentials(options = {}) {
return Object.assign(options, {
credentials: 'include',
mode: 'same-origin',
headers: Object.assign((options.headers || {}), authenticityHeader(), jsonHeader()),
});
}
export function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response.json();
}

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<script src="https://kit.fontawesome.com/f2ba80f106.js" crossorigin="anonymous"></script>
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_pack_tag 'application' %>

View File

@ -1,2 +1,4 @@
<%= javascript_pack_tag 'home' %>
<%= stylesheet_pack_tag 'home' %>

View File

@ -0,0 +1,2 @@
<%= javascript_pack_tag 'login' %>
<%= stylesheet_pack_tag 'login' %>

View File

@ -1 +0,0 @@
<%= javascript_pack_tag 'tweets' %>

View File

@ -1,6 +1,9 @@
Rails.application.routes.draw do
root 'static_pages#home'
get '/@:user' => 'static_pages#user'
get '/login' => 'static_pages#login'
namespace :api do
# USERS
post '/users' => 'users#create'

View File

@ -5,3 +5,17 @@
#
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first)
users = User.create([
{username: 'jack123', email: 'jack123@test.com', password: 'password'},
{username: 'bob123', email: 'bob123@test.com', password: 'password'},
{username: 'liz123', email: 'liz123@test.com', password: 'password'}
])
tweets = Tweet.create([
{message: 'some sample text', user: users.first},
{message: 'i love coding!', user: users.first},
{message: 'just joined twitter!', user: users.second},
{message: 'seems cool', user: users.second},
{message: 'knock knock', user: users.third},
{message: 'its me!', user: users.third},
])