Fetch:Login/Logout, getTweets, deleteTweet
This commit is contained in:
parent
3f61aae359
commit
634e59883a
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module Api
|
||||
class SessionsController < ApplicationController
|
||||
class SessionsController < ApplicationController
|
||||
def create
|
||||
@user = User.find_by(username: params[:user][:username])
|
||||
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
class StaticPagesController < ApplicationController
|
||||
def home
|
||||
render 'home'
|
||||
end
|
||||
|
||||
def login
|
||||
render 'login'
|
||||
end
|
||||
|
||||
def user
|
||||
render 'user'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
import '@src/login'
|
|
@ -1 +0,0 @@
|
|||
import '@src/tweets'
|
|
@ -0,0 +1 @@
|
|||
import '@src/user'
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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')),
|
||||
)
|
||||
})
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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')),
|
||||
)
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
const User = () => {
|
||||
return (
|
||||
<h1>User page</h1>
|
||||
)
|
||||
}
|
||||
|
||||
export default User;
|
|
@ -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();
|
||||
}
|
|
@ -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' %>
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
<%= javascript_pack_tag 'home' %>
|
||||
<%= stylesheet_pack_tag 'home' %>
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<%= javascript_pack_tag 'login' %>
|
||||
<%= stylesheet_pack_tag 'login' %>
|
|
@ -1 +0,0 @@
|
|||
<%= javascript_pack_tag 'tweets' %>
|
|
@ -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'
|
||||
|
|
14
db/seeds.rb
14
db/seeds.rb
|
@ -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},
|
||||
])
|
Loading…
Reference in New Issue