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
1 change: 1 addition & 0 deletions .anima/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cache
28,491 changes: 22,124 additions & 6,367 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"test": "react-scripts test"
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
Expand All @@ -27,12 +28,18 @@
"postcss-focus-visible": "^9.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-router-dom": "^6.0.0",
"react-scripts": "5.0.1",
"remark-gfm": "^3.0.1",
"tailwindcss": "^3.2.1"
},
"devDependencies": {
"@testing-library/dom": "^8.11.1",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"eslint": "^8.45.0",
"eslint-config-next": "^13.4.12",
"jest": "^29.7.0",
"prettier": "^3.0.0",
"prettier-plugin-tailwindcss": "^0.4.1"
}
Expand Down
62 changes: 62 additions & 0 deletions src/components/NewPost.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const NewPost = () => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [errors, setErrors] = useState([]);
const navigate = useNavigate();

const handleSubmit = async (event) => {
event.preventDefault();
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description })
});

if (response.ok) {
const { post } = await response.json();
navigate(`/posts/${post.id}`);
} else {
const { errors } = await response.json();
setErrors(errors);
}
};

return (
<div>
<h1>Create New Post</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">Title</label>
<input
id="title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div>
<label htmlFor="description">Description</label>
<input
id="description"
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<button type="submit">Create Post</button>
</form>
{errors.length > 0 && (
<ul>
{errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
)}
</div>
);
};

export default NewPost;
47 changes: 47 additions & 0 deletions src/components/NewPost.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import NewPost from './NewPost';

test('renders form and handles submission correctly', async () => {
await act(async () => {
render(
<MemoryRouter>
<NewPost />
</MemoryRouter>
);
});

// Check if the form elements are rendered
const titleInput = screen.getByLabelText(/title/i);
const descriptionInput = screen.getByLabelText(/description/i);
const createPostButton = screen.getByText(/create post/i);

expect(titleInput).toBeInTheDocument();
expect(descriptionInput).toBeInTheDocument();
expect(createPostButton).toBeInTheDocument();

// Simulate filling out the form
fireEvent.change(titleInput, { target: { value: 'Test Title' } });
fireEvent.change(descriptionInput, { target: { value: 'Test Description' } });

// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ post: { id: 1, title: 'Test Title', description: 'Test Description' } }),
})
);

// Simulate form submission
await act(async () => {
fireEvent.click(createPostButton);
});

// Check if fetch was called with correct arguments
expect(global.fetch).toHaveBeenCalledWith('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Test Title', description: 'Test Description' }),
});
});
16 changes: 14 additions & 2 deletions src/pages/posts/[slug].jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ArticleLayout } from '@/components/ArticleLayout'
import Link from 'next/link' // Import Link from Next.js
import { getPost, getPosts } from "@/api/postsApi"
import { postParameters } from "@/lib/postUtilities"

Expand All @@ -25,14 +26,25 @@ export default function Post({ post }) {

const meta = {
author: 'Spencer Sharp',
date: post.createdDate,
title: post.title,
description: post.body,
}

return (
<ArticleLayout meta={meta}>
{post.body}
{/* Render the post content */}
<div className="post-body">
{post.body}
</div>

{/* Add a "Create New Post" button */}
<div className="mt-8">
<Link href="/posts/new">
<a className="inline-block px-4 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600">
Create New Post
</a>
</Link>
</div>
</ArticleLayout>
)
}
14 changes: 12 additions & 2 deletions src/pages/posts/index.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Head from 'next/head'

import Link from 'next/link' // Import Link from Next.js
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import { formatDate } from '@/lib/formatDate'
import { getPosts, getPost } from "@/api/postsApi"
import { getPosts } from '@/api/postsApi'

function Post({ post }) {
const date = new Date(post.createdDate)
Expand Down Expand Up @@ -50,6 +50,16 @@ export default function PostsIndex({ posts }) {
title="Writing on software design, company building, and the aerospace industry."
intro="All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order."
>

{/* Add the "Create New Post" Button */}
<div className="mb-8">
<Link href="/posts/new">
<id className="inline-block px-4 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600">
Create New Post
</id>
</Link>
</div>

<div className="md:border-l md:border-zinc-100 md:pl-6">
<div className="flex max-w-3xl flex-col space-y-16">
{posts.map((post) => (
Expand Down
116 changes: 116 additions & 0 deletions src/pages/posts/new/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useState } from 'react'
import { useRouter } from 'next/router'
import Head from 'next/head'
import Link from 'next/link'

export default function NewPost() {
const router = useRouter()

// State for form fields
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')

// State for errors
const [errors, setErrors] = useState([])

// Handle form submission
const handleSubmit = async (event) => {
event.preventDefault()

// Prepare the post data
const postData = { title, description }

try {
// Call backend API to create post
const response = await fetch('http://localhost:5000/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
})

const data = await response.json()

// Handle success
if (response.ok) {
// Redirect to the new post
router.push(`/posts/${data.post.slug}`)
} else {
// Set validation errors if any
setErrors(data.errors || [])
}
} catch (error) {
console.error('Error creating post:', error)
}
}

return (
<>
<Head>
<title>Create New Post</title>
</Head>

<div className="max-w-2xl mx-auto mt-12">
<h1 className="text-3xl font-bold mb-6">Create New Post</h1>

{/* Display validation errors, if any */}
{errors.length > 0 && (
<div className="mb-4 p-4 bg-red-100 text-red-700">
<ul>
{errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</div>
)}

<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="title" className="block text-sm font-medium">
Title
</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full mt-1 p-2 border rounded-md"
required
/>
</div>

<div>
<label htmlFor="description" className="block text-sm font-medium">
Description
</label>
<textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full mt-1 p-2 border rounded-md"
rows="5"
required
/>
</div>

<div>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
>
Create Post
</button>

{/* Optional cancel button */}
<Link href="/posts">
<id className="ml-4 px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600">
Cancel
</id>
</Link>
</div>
</form>
</div>
</>
)
}
2 changes: 2 additions & 0 deletions src/setupTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// src/setupTests.js
import '@testing-library/jest-dom/';