-
-
Save codecademydev/7ca40c1fa1d46ba2face90c66730d324 to your computer and use it in GitHub Desktop.
Codecademy export
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState } from 'react'; | |
import { useDispatch, useSelector } from 'react-redux'; | |
import { | |
createCommentIsPending, | |
postCommentForArticleId | |
} from '../features/comments/commentsSlice'; | |
export default function CommentForm({ articleId }) { | |
const dispatch = useDispatch(); | |
const [comment, setComment] = useState(''); | |
// Declare isCreatePending here. | |
const isCreatePending = useSelector(createCommentIsPending); | |
const handleSubmit = (e) => { | |
e.preventDefault(); | |
// dispatch your asynchronous action here! | |
dispatch(postCommentForArticleId({articleId, comment})); | |
setComment(''); | |
}; | |
return ( | |
<form onSubmit={handleSubmit}> | |
<label for='comment' className='label'> | |
Add Comment: | |
</label> | |
<div id='input-container'> | |
<input | |
id='comment' | |
value={comment} | |
onChange={(e) => setComment(e.currentTarget.value)} | |
type='text' | |
/> | |
<button | |
disabled={isCreatePending} | |
className='comment-button' | |
> | |
Submit | |
</button> | |
</div> | |
</form> | |
); | |
} |
Raw
CommentList.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Show hidden characters
import React from 'react'; | |
import Comment from './Comment'; | |
export default function CommentList({ comments }) { | |
if (!comments) { | |
return null; | |
} | |
return ( | |
<ul className='comments-list'> | |
{ | |
comments.map(comment => { | |
return <li> <Comment comment={comment} /> </li> | |
}) | |
} | |
</ul> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect } from 'react'; | |
import { useDispatch, useSelector } from 'react-redux'; | |
import { | |
loadCommentsForArticleId, | |
selectComments, | |
isLoadingComments, | |
} from '../comments/commentsSlice'; | |
import { selectCurrentArticle } from '../currentArticle/currentArticleSlice'; | |
import CommentList from '../../components/CommentList'; | |
import CommentForm from '../../components/CommentForm'; | |
const Comments = () => { | |
const dispatch = useDispatch(); | |
const article = useSelector(selectCurrentArticle); | |
// Declare additional selected data here. | |
const comments = useSelector(selectComments); | |
const commentsAreLoading = useSelector(isLoadingComments); | |
// Dispatch loadCommentsForArticleId with useEffect here. | |
useEffect(() => { | |
if ( article !== undefined ) { | |
dispatch(loadCommentsForArticleId(article.id))}; | |
}, [dispatch, article]); | |
const commentsForArticleId = article === undefined ? [] : comments[article.id]; | |
if (commentsAreLoading) return <div>Loading Comments</div>; | |
if (!article) return null; | |
return ( | |
<div className='comments-container'> | |
<h3 className='comments-title'>Comments</h3> | |
<CommentList comments={commentsForArticleId} /> | |
<CommentForm articleId={article.id} /> | |
</div> | |
); | |
}; | |
export default Comments; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import createAsyncThunk and createSlice here. | |
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; | |
// Create loadCommentsForArticleId here. | |
export const loadCommentsForArticleId = createAsyncThunk ( | |
'comments/loadCommentForArticleId', | |
async (id) => { | |
const response = await fetch (`api/articles/${id}/comments`); | |
const json = await response.json(); | |
return json; | |
} | |
) | |
// Create postCommentForArticleId here. | |
export const postCommentForArticleId = createAsyncThunk ( | |
'comments/postCommentForArticleId', | |
async ({articleId, comment}) => { | |
const requestBody = await JSON.stringify({comment: comment}); | |
const response = await fetch(`api/articles/${articleId}/comments`, { | |
method:'POST', | |
body: requestBody} | |
); | |
const json = await response.json(); | |
return json; | |
} | |
) | |
export const commentsSlice = createSlice({ | |
name: 'comments', | |
initialState: { | |
// Add initial state properties here. | |
byArticleId: {}, | |
isLoadingComments: false, | |
failedToLoadComments: false, | |
createCommentIsPending: false, | |
failedToCreateComment: false, | |
}, | |
// Add extraReducers here. | |
extraReducers: { | |
[loadCommentsForArticleId.pending]: (state) => { | |
state.isLoadingComments = true; | |
state.failedToLoadComments = false; | |
}, | |
[loadCommentsForArticleId.fulfilled]: (state, action) => { | |
state.isLoadingComments = false; | |
state.failedToLoadComments = false; | |
state.byArticleId[action.payload.articleId] = action.payload.comments; | |
}, | |
[loadCommentsForArticleId.rejected]: (state) => { | |
state.isLoadingComments = false; | |
state.failedToLoadComments = false; | |
}, | |
[postCommentForArticleId.pending]: (state) => { | |
state.createCommentIsPending = true; | |
state.failedToCreateComment = false; | |
}, | |
[postCommentForArticleId.fulfilled]: (state, action) => { | |
state.createCommentIsPending = false; | |
state.failedToCreateComment = false; | |
state.byArticleId[action.payload.articleId].push(action.payload) | |
}, | |
[postCommentForArticleId.rejected]: (state) => { | |
state.createCommentIsPending = false; | |
state.failedToCreateComment = true; | |
} | |
} | |
}); | |
export const selectComments = (state) => state.comments.byArticleId; | |
export const isLoadingComments = (state) => state.comments.isLoadingComments; | |
export const createCommentIsPending = (state) => state.comments.createCommentIsPending; | |
export default commentsSlice.reducer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { rest } from 'msw'; | |
import articlesData from './articles.json'; | |
import commentsData from './comments.json'; | |
const userComments = {}; | |
function mockDelay(milliseconds) { | |
const date = Date.now(); | |
let currentDate = null; | |
do { | |
currentDate = Date.now(); | |
} while (currentDate - date < milliseconds); | |
} | |
export const handlers = [ | |
rest.get('/api/articles', (req, res, ctx) => { | |
mockDelay(500); | |
return res( | |
ctx.status(200), | |
ctx.json( | |
articlesData.map((article) => ({ | |
id: article.id, | |
title: article.title, | |
preview: article.preview, | |
image: article.image, | |
})) | |
) | |
); | |
}), | |
rest.get('/api/articles/:articleId', (req, res, ctx) => { | |
mockDelay(500); | |
const { articleId } = req.params; | |
return res( | |
ctx.status(200), | |
ctx.json( | |
articlesData.find((article) => article.id === parseInt(articleId)) | |
) | |
); | |
}), | |
rest.get('/api/articles/:articleId/comments', (req, res, ctx) => { | |
mockDelay(500); | |
const { articleId } = req.params; | |
const userCommentsForArticle = userComments[articleId] || []; | |
return res( | |
ctx.status(200), | |
ctx.json({ | |
articleId: parseInt(articleId), | |
comments: commentsData | |
.filter((comment) => comment.articleId === parseInt(articleId)) | |
.concat(userCommentsForArticle), | |
}) | |
); | |
}), | |
rest.post('/api/articles/:articleId/comments', (req, res, ctx) => { | |
mockDelay(500); | |
const { articleId } = req.params; | |
const commentResponse = { | |
id: commentsData.length, | |
articleId: parseInt(articleId), | |
text: JSON.parse(req.body).comment, | |
}; | |
if (userComments[articleId]) { | |
userComments[articleId].push(commentResponse); | |
} else { | |
userComments[articleId] = [commentResponse]; | |
} | |
return res(ctx.status(200), ctx.json(commentResponse)); | |
}), | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Mock Service Worker. | |
* @see https://github.com/mswjs/msw | |
* - Please do NOT modify this file. | |
* - Please do NOT serve this file on production. | |
*/ | |
/* eslint-disable */ | |
/* tslint:disable */ | |
const INTEGRITY_CHECKSUM = '65d33ca82955e1c5928aed19d1bdf3f9' | |
const bypassHeaderName = 'x-msw-bypass' | |
let clients = {} | |
self.addEventListener('install', function () { | |
return self.skipWaiting() | |
}) | |
self.addEventListener('activate', async function (event) { | |
return self.clients.claim() | |
}) | |
self.addEventListener('message', async function (event) { | |
const clientId = event.source.id | |
const client = await event.currentTarget.clients.get(clientId) | |
const allClients = await self.clients.matchAll() | |
const allClientIds = allClients.map((client) => client.id) | |
switch (event.data) { | |
case 'KEEPALIVE_REQUEST': { | |
sendToClient(client, { | |
type: 'KEEPALIVE_RESPONSE', | |
}) | |
break | |
} | |
case 'INTEGRITY_CHECK_REQUEST': { | |
sendToClient(client, { | |
type: 'INTEGRITY_CHECK_RESPONSE', | |
payload: INTEGRITY_CHECKSUM, | |
}) | |
break | |
} | |
case 'MOCK_ACTIVATE': { | |
clients = ensureKeys(allClientIds, clients) | |
clients[clientId] = true | |
sendToClient(client, { | |
type: 'MOCKING_ENABLED', | |
payload: true, | |
}) | |
break | |
} | |
case 'MOCK_DEACTIVATE': { | |
clients = ensureKeys(allClientIds, clients) | |
clients[clientId] = false | |
break | |
} | |
case 'CLIENT_CLOSED': { | |
const remainingClients = allClients.filter((client) => { | |
return client.id !== clientId | |
}) | |
// Unregister itself when there are no more clients | |
if (remainingClients.length === 0) { | |
self.registration.unregister() | |
} | |
break | |
} | |
} | |
}) | |
self.addEventListener('fetch', function (event) { | |
const { clientId, request } = event | |
const requestClone = request.clone() | |
const getOriginalResponse = () => fetch(requestClone) | |
// Bypass navigation requests. | |
if (request.mode === 'navigate') { | |
return | |
} | |
// Bypass mocking if the current client isn't present in the internal clients map | |
// (i.e. has the mocking disabled). | |
if (!clients[clientId]) { | |
return | |
} | |
// Opening the DevTools triggers the "only-if-cached" request | |
// that cannot be handled by the worker. Bypass such requests. | |
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { | |
return | |
} | |
event.respondWith( | |
new Promise(async (resolve, reject) => { | |
const client = await event.target.clients.get(clientId) | |
// Bypass mocking when the request client is not active. | |
if (!client) { | |
return resolve(getOriginalResponse()) | |
} | |
// Bypass requests with the explicit bypass header | |
if (requestClone.headers.get(bypassHeaderName) === 'true') { | |
const modifiedHeaders = serializeHeaders(requestClone.headers) | |
// Remove the bypass header to comply with the CORS preflight check | |
delete modifiedHeaders[bypassHeaderName] | |
const originalRequest = new Request(requestClone, { | |
headers: new Headers(modifiedHeaders), | |
}) | |
return resolve(fetch(originalRequest)) | |
} | |
const reqHeaders = serializeHeaders(request.headers) | |
const body = await request.text() | |
const rawClientMessage = await sendToClient(client, { | |
type: 'REQUEST', | |
payload: { | |
url: request.url, | |
method: request.method, | |
headers: reqHeaders, | |
cache: request.cache, | |
mode: request.mode, | |
credentials: request.credentials, | |
destination: request.destination, | |
integrity: request.integrity, | |
redirect: request.redirect, | |
referrer: request.referrer, | |
referrerPolicy: request.referrerPolicy, | |
body, | |
bodyUsed: request.bodyUsed, | |
keepalive: request.keepalive, | |
}, | |
}) | |
const clientMessage = rawClientMessage | |
switch (clientMessage.type) { | |
case 'MOCK_SUCCESS': { | |
setTimeout( | |
resolve.bind(this, createResponse(clientMessage)), | |
clientMessage.payload.delay, | |
) | |
break | |
} | |
case 'MOCK_NOT_FOUND': { | |
return resolve(getOriginalResponse()) | |
} | |
case 'NETWORK_ERROR': { | |
const { name, message } = clientMessage.payload | |
const networkError = new Error(message) | |
networkError.name = name | |
// Rejecting a request Promise emulates a network error. | |
return reject(networkError) | |
} | |
case 'INTERNAL_ERROR': { | |
const parsedBody = JSON.parse(clientMessage.payload.body) | |
console.error( | |
`\ | |
[MSW] Request handler function for "%s %s" has thrown the following exception: | |
${parsedBody.errorType}: ${parsedBody.message} | |
(see more detailed error stack trace in the mocked response body) | |
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. | |
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ | |
`, | |
request.method, | |
request.url, | |
) | |
return resolve(createResponse(clientMessage)) | |
} | |
} | |
}).catch((error) => { | |
console.error( | |
'[MSW] Failed to mock a "%s" request to "%s": %s', | |
request.method, | |
request.url, | |
error, | |
) | |
}), | |
) | |
}) | |
function serializeHeaders(headers) { | |
const reqHeaders = {} | |
headers.forEach((value, name) => { | |
reqHeaders[name] = reqHeaders[name] | |
? [].concat(reqHeaders[name]).concat(value) | |
: value | |
}) | |
return reqHeaders | |
} | |
function sendToClient(client, message) { | |
return new Promise((resolve, reject) => { | |
const channel = new MessageChannel() | |
channel.port1.onmessage = (event) => { | |
if (event.data && event.data.error) { | |
reject(event.data.error) | |
} else { | |
resolve(event.data) | |
} | |
} | |
client.postMessage(JSON.stringify(message), [channel.port2]) | |
}) | |
} | |
function createResponse(clientMessage) { | |
return new Response(clientMessage.payload.body, { | |
...clientMessage.payload, | |
headers: clientMessage.payload.headers, | |
}) | |
} | |
function ensureKeys(keys, obj) { | |
return Object.keys(obj).reduce((acc, key) => { | |
if (keys.includes(key)) { | |
acc[key] = obj[key] | |
} | |
return acc | |
}, {}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment