Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions frontend/axios/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
// ✨ implement axiosWithAuth
import axios from "axios";

export function axiosWithAuth(){
const token = localStorage.getItem('token')
return axios.create({
headers: {
Authorization: token,
},
})
}
100 changes: 84 additions & 16 deletions frontend/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import LoginForm from './LoginForm'
import Message from './Message'
import ArticleForm from './ArticleForm'
import Spinner from './Spinner'
import axios from 'axios'
import { axiosWithAuth } from '../axios'

const articlesUrl = 'http://localhost:9000/api/articles'
const loginUrl = 'http://localhost:9000/api/login'
Expand All @@ -18,10 +20,14 @@ export default function App() {

// ✨ Research `useNavigate` in React Router v.6
const navigate = useNavigate()
const redirectToLogin = () => { /* ✨ implement */ }
const redirectToArticles = () => { /* ✨ implement */ }
const redirectToLogin = () => {navigate('/')}
const redirectToArticles = () => {navigate('/articles')}

const logout = () => {
localStorage.removeItem('token')
setMessage('Goodbye!')
redirectToLogin();
setArticles([])
// ✨ implement
// If a token is in local storage it should be removed,
// and a message saying "Goodbye!" should be set in its proper state.
Expand All @@ -30,15 +36,36 @@ export default function App() {
}

const login = ({ username, password }) => {
// ✨ implement
// We should flush the message state, turn on the spinner
// and launch a request to the proper endpoint.
// On success, we should set the token to local storage in a 'token' key,
// put the server success message in its proper state, and redirect
// to the Articles screen. Don't forget to turn off the spinner!
const credentials = {
username: username,
password: password
}
setSpinnerOn(true)
axios.post(loginUrl,credentials)
.then(({data}) => {
setMessage(data.message)
localStorage.setItem('token', data.token)
redirectToArticles()
setSpinnerOn(false)
})
.catch((err) => {
console.log(err)
setMessage('error: please try again')
setSpinnerOn(false)
})

}

const getArticles = () => {
const getArticles = (mess) => {
setSpinnerOn(true)
axiosWithAuth().get(articlesUrl)
.then(({data}) => {
if(!mess){
setMessage(data.message)
}
setArticles(data.articles)
setSpinnerOn(false)
})
// ✨ implement
// We should flush the message state, turn on the spinner
// and launch an authenticated request to the proper endpoint.
Expand All @@ -50,26 +77,63 @@ export default function App() {
}

const postArticle = article => {
const mess = true
// ✨ implement
// The flow is very similar to the `getArticles` function.
// You'll know what to do! Use log statements or breakpoints
// to inspect the response from the server.
setSpinnerOn(true)
return axiosWithAuth().post(articlesUrl, article)
.then(({data}) => {
getArticles(mess);
setMessage(data.message)
setSpinnerOn(false)
})
.catch(err => {
setSpinnerOn(false)
console.error(err)
})
}

const updateArticle = ({ article_id, article }) => {
const updateArticle = ({article_id, article}) => {
const mess = true
// ✨ implement
// You got this!
setSpinnerOn(true)
return axiosWithAuth().put(`${articlesUrl}/${article_id}`, article)
.then(({data}) => {
getArticles(mess);
setMessage(data.message)
setCurrentArticleId()
setSpinnerOn(false)
})
.catch(err => {
setSpinnerOn(false)
console.error(err)
})

}

const deleteArticle = article_id => {
const mess = true
// ✨ implement
setSpinnerOn(true)
return axiosWithAuth().delete(`${articlesUrl}/${article_id}`)
.then(({data}) => {
getArticles(mess);
setMessage(data.message)
setSpinnerOn(false)
})
.catch(err => {
console.error(err)
setSpinnerOn(false)
})
}

return (
// ✨ fix the JSX: `Spinner`, `Message`, `LoginForm`, `ArticleForm` and `Articles` expect props ❗
<>
<Spinner />
<Message />
<Spinner on={spinnerOn}/>
<Message message={message}/>
<button id="logout" onClick={logout}>Logout from app</button>
<div id="wrapper" style={{ opacity: spinnerOn ? "0.25" : "1" }}> {/* <-- do not change this line */}
<h1>Advanced Web Applications</h1>
Expand All @@ -78,11 +142,15 @@ export default function App() {
<NavLink id="articlesScreen" to="/articles">Articles</NavLink>
</nav>
<Routes>
<Route path="/" element={<LoginForm />} />
<Route path="/" element={<LoginForm login={login}/>} />
<Route path="articles" element={
<>
<ArticleForm />
<Articles />
<ArticleForm currentArticleId={currentArticleId}
articles={articles}
setCurrentArticleId={setCurrentArticleId}
updateArticle={updateArticle}
postArticle={postArticle}/>
<Articles articles={articles} getArticles={getArticles} setCurrentArticleId={setCurrentArticleId} deleteArticle={deleteArticle}/>
</>
} />
</Routes>
Expand Down
43 changes: 41 additions & 2 deletions frontend/components/ArticleForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,29 @@ const initialFormValues = { title: '', text: '', topic: '' }
export default function ArticleForm(props) {
const [values, setValues] = useState(initialFormValues)
// ✨ where are my props? Destructure them here
const { currentArticleId,
articles,
setCurrentArticleId,
updateArticle,
postArticle
} = props

useEffect(() => {
setValues(initialFormValues)
}, [])

useEffect(() => {
// ✨ implement
// Every time the `currentArticle` prop changes, we should check it for truthiness:
// if it's truthy, we should set its title, text and topic into the corresponding
// values of the form. If it's not, we should reset the form back to initial values.
})
if(currentArticleId){
const currentArticle = articles.filter(art => art.article_id === currentArticleId)
setValues(currentArticle[0])
}else{
setValues(initialFormValues)
}
}, [currentArticleId])

const onChange = evt => {
const { id, value } = evt.target
Expand All @@ -21,16 +37,39 @@ export default function ArticleForm(props) {

const onSubmit = evt => {
evt.preventDefault()
console.log(evt)
if(currentArticleId){
let data = {
article_id: currentArticleId,
article: values
}
setValues(initialFormValues)
return updateArticle(data)
}else{
postArticle(values)
setValues(initialFormValues)
}
// ✨ implement
// We must submit a new post or update an existing one,
// depending on the truthyness of the `currentArticle` prop.
}

const isDisabled = () => {
if(values.text && values.topic && values.title){
return false
}else{
return true
}
// ✨ implement
// Make sure the inputs have some values
}

const cancel = (e) => {
e.preventDefault();
setCurrentArticleId();
setValues(initialFormValues)
}

return (
// ✨ fix the JSX: make the heading display either "Edit" or "Create"
// and replace Function.prototype with the correct function
Expand Down Expand Up @@ -58,7 +97,7 @@ export default function ArticleForm(props) {
</select>
<div className="button-group">
<button disabled={isDisabled()} id="submitArticle">Submit</button>
<button onClick={Function.prototype}>Cancel edit</button>
<button onClick={(e) => cancel(e)}>Cancel edit</button>
</div>
</form>
)
Expand Down
30 changes: 24 additions & 6 deletions frontend/components/Articles.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import React, { useEffect } from 'react'
import { Navigate } from 'react-router-dom'
import { useNavigate } from 'react-router-dom'
import PT from 'prop-types'

export default function Articles(props) {
const navigate = useNavigate()
const redirectToLogin = () => {navigate('/')}
const {articles, getArticles, setCurrentArticleId, deleteArticle} = props;
// ✨ where are my props? Destructure them here

// ✨ implement conditional logic: if no token exists
// we should render a Navigate to login screen (React Router v.6)

useEffect(() => {
// ✨ grab the articles here, on first render only
})
if(!localStorage.getItem('token')){
return redirectToLogin()
}
if(!articles.length){
return getArticles();
}
}, [])

function isAuthy(){
const token = localStorage.getItem('token')
if(!token){
return true
}else{
return false
}
}

return (
// ✨ fix the JSX: replace `Function.prototype` with actual functions
// and use the articles prop to generate articles
<div className="articles">
<h2>Articles</h2>
{
![].length
!articles.length
? 'No articles yet'
: [].map(art => {
: articles.map(art => {
return (
<div className="article" key={art.article_id}>
<div>
Expand All @@ -29,8 +47,8 @@ export default function Articles(props) {
<p>Topic: {art.topic}</p>
</div>
<div>
<button disabled={true} onClick={Function.prototype}>Edit</button>
<button disabled={true} onClick={Function.prototype}>Delete</button>
<button disabled={isAuthy()} onClick={() => setCurrentArticleId(art.article_id)}>Edit</button>
<button disabled={isAuthy()} onClick={() => deleteArticle(art.article_id)}>Delete</button>
</div>
</div>
)
Expand Down
9 changes: 9 additions & 0 deletions frontend/components/LoginForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const initialFormValues = {
export default function LoginForm(props) {
const [values, setValues] = useState(initialFormValues)
// ✨ where are my props? Destructure them here
const {login} = props;

const onChange = evt => {
const { id, value } = evt.target
Expand All @@ -16,10 +17,17 @@ export default function LoginForm(props) {

const onSubmit = evt => {
evt.preventDefault()
login(values)
setValues(initialFormValues);
// ✨ implement
}

const isDisabled = () => {
if(values.username.trim().length >= 3 && values.password.trim().length >= 8){
return false
}else{
return true
}
// ✨ implement
// Trimmed username must be >= 3, and
// trimmed password must be >= 8 for
Expand All @@ -38,6 +46,7 @@ export default function LoginForm(props) {
/>
<input
maxLength={20}
type='password'
value={values.password}
onChange={onChange}
placeholder="Enter password"
Expand Down
18 changes: 17 additions & 1 deletion frontend/components/Spinner.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
// Import the Spinner component into this file and test
// that it renders what it should for the different props it can take.
import Spinner from "./Spinner"
import React from "react"
import { render, screen } from "@testing-library/react"

test('sanity', () => {
expect(true).toBe(false)
expect(true).toBe(true)
})

test('spinner renders', () => {
render(<Spinner on={true}/>)
})

test('spinner does not render',() => {
render(<Spinner on={false} />)
})

//What are you even asking me to find? if it isn't on, then it's null so there's nothing to render....


Loading