Our great sponsors
-
Here is the source code for the working application.
-
import { useState, useEffect } from 'react' import { useRouter } from 'next/router' import { Button, Textarea, Input, FormControl, FormLabel, Container, Text, Spacer, Icon, Link, Spinner, Alert, AlertDescription, AlertIcon, AlertTitle } from '@chakra-ui/react' import { FaGithub } from 'react-icons/fa' import { toast } from 'react-toastify' import generateSocialImage from '../../components/GenerateImg' import useSWR from 'swr' const fetcher = (...args) => fetch(...args).then((res) => res.json()) const UpdatePost = () => { const [title, setTitle] = useState(''); const [body, setBody] = useState(''); const [tags, setTags] = useState(''); const router = useRouter(); const { id } = router.query; //Get data from xata db const { data, error } = useSWR(`/api/post?id=${id}`, fetcher) if (error) return ( Error! Failed to Load. ) if (!data) return // store data in state const res = data.post; // handle form submit const handleSubmit = async () => { //Convert string tags to array const newTags = tags || res.tags.toString(); console.log(newTags) // Reducing number of accepted tags to 4 if user inputs more const tagArr = newTags.split(/[, ]+/); let tags_new; if (tagArr.length >= 4) { tags_new = tagArr.slice(0, 4) } else tags_new = tagArr; console.log(tags_new); //Generate social card with cloudinary const socialImage = generateSocialImage({ title: title || res.title, tagline: tags_new.map(tag => `#${tag}`).join(' '), cloudName: 'dqwrnan7f', imagePublicID: 'dex/example-black_iifqhm', }); console.log(socialImage); //Make add create request let post = { title: title || res.title, body: body || res.body, image: socialImage, tags: tags_new, } const response = await fetch('/api/update', { method: 'POST', headers: { "Content-Type": "application/json", }, body: JSON.stringify({ post, id }) }) if (response.ok) { toast.success("post updated successfully", { theme: "dark", autoClose: 8000 }) window?.location.replace('/'); } } return ( Blog with Xata and Cloudinary Post Title { setTitle(e.target.value) }} /> Post Tags { setTags(e.target.value) }} /> Post Body { setBody(e.target.value) }} /> handleSubmit()}>Submit ) } export default UpdatePost
Enter fullscreen mode Exit fullscreen modeFor our other reusable components
AllPosts.js
andCreateModal.js
, we have:AllPost.js
import NextLink from 'next/link' import { Box, Image, Badge, Flex, Spacer, ButtonGroup, Link } from '@chakra-ui/react'; import { DeleteIcon, EditIcon, ExternalLinkIcon } from '@chakra-ui/icons' import { toast } from 'react-toastify' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' const AllPosts = ({ posts }) => { const deleteData = async (id) => { const { status } = await fetch('/api/delete', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id }), }) if (status === 200) { toast.success("post deleted successfully", { theme: "dark", autoClose: 5000 }) } window?.location.reload() } return (
{ posts && posts.map((post, index) => { return () } export default AllPostsTags: {post.tags.length > 0 && post.tags.map((tag, index) => { return ( {(index ? ',' : '') + ' ' + tag} ) })} deleteData(post.id)} mt={1} /> {post.title} {post.body}) }) }Enter fullscreen mode Exit fullscreen modeCreateModal.js
import { useState } from 'react' import { useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Button, Textarea, Input, FormControl, FormLabel, } from '@chakra-ui/react' import { toast } from 'react-toastify' import generateSocialImage from './GenerateImg' const PostForm = () => { const [title, setTitle] = useState(""); const [body, setBody] = useState(""); const [tags, setTags] = useState(""); //Convert string tags to array const newTags = tags; const handleSubmit = async () => { if (title == '' || body == '' || tags == '') { toast.warn("post cannot be empty", { theme: "dark", autoClose: 8000 }) } else { const tagArr = newTags.split(/[, ]+/); let tags_new; if (tagArr.length >= 4) { tags_new = tagArr.slice(0, 4) } else tags_new = tagArr; console.log(tags_new); //Generate social card const socialImage = generateSocialImage({ title: title, tagline: tags_new.map(tag => `#${tag}`).join(' '), cloudName: 'dqwrnan7f', imagePublicID: 'dex/example-black_iifqhm', }); console.log(socialImage); //Make add create request let posts = { title: title, body: body, image: socialImage, tags: tags_new, } const response = await fetch('/api/create', { method: 'POST', headers: { "Content-Type": "application/json", }, body: JSON.stringify(posts) }) if (response.ok) { toast.success("post created successfully", { theme: "dark", autoClose: 8000 }) window?.location.reload() } } } return ( <> Post Title { setTitle(e.target.value) }} required /> Post Tags { setTags(e.target.value) }} required /> Post Body { setBody(e.target.value) }} required /> handleSubmit()}>Submit > ) } const CreatePost = () => { const { isOpen, onOpen, onClose } = useDisclosure() return ( <> Create Post Modal Title > ) } export default CreatePost
Enter fullscreen mode Exit fullscreen modeWe used our
generateSocialImage
Cloudinary function inside thecreateModal
component andupdate
page to generate a social card. Here is an isolated version:
//Generate social card const socialImage = generateSocialImage({ title: title, tagline: tags_new.map(tag => `#${tag}`).join(' '), cloudName: 'dqwrnan7f', imagePublicID: 'dex/example-black_iifqhm', }); console.log(socialImage);
Enter fullscreen mode Exit fullscreen modeWe can see how we passed dynamic data to the function, our public image id, cloudname, title from our blog, and taglines from our blogpost too. Our social card will look like this when the function is executed:
If you followed up till this point, our application is almost ready! Finally, we will go ahead and add some CSS styles to the default stylesheet file in our project
globals.css
:
html, body { padding: 0; margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; } a { color: inherit; text-decoration: none; } * { box-sizing: border-box; } @media (prefers-color-scheme: dark) { html { color-scheme: dark; } body { color: white; background: black; } } .main { padding: 5rem 0; flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; } .footer { width: 100%; height: 100px; border-top: 1px solid #eaeaea; display: flex; justify-content: center; align-items: center; } .footer img { margin-left: 0.5rem; } .footer a { display: flex; justify-content: center; align-items: center; } .grid { display: flex; flex-direction: row; align-items: center; justify-content: center; flex-wrap: wrap; max-width: 80%; margin-top: 3rem; margin: auto; } .card:hover, .card:focus, .card:active { color: #0070f3; border-color: #0070f3; } @media (max-width: 600px) { .grid { max-width: 100%; flex-direction: column; } }
Enter fullscreen mode Exit fullscreen modeWe will then run our application using any of these commands to see the finished product.
# NPM npm run dev # Yarn yarn run dev
Enter fullscreen mode Exit fullscreen modeNow we have our blog running on Xata and Cloudinary serverless provisions. We can go ahead and improve the user interface, make it more responsive and even add some animations too. We can also host it on services like Netlify, Vercel, and any other client-side hosting platforms we can think of. For this article, we will be deploying to Netlify.
One easy way to deploy to Netlify is to push our code to Github, connect our Github to Netlify and select the repository that contains our project. We will select the Next.js preset build command and everything will run and deploy automatically with fewer or no configurations. Check this article for more insight on deploying to Netlify.
Our live link on Netlify is ready.
Conclusion
So we were able to learn from this article how we can use Jamstack through Next.js, Cloudinary, Xata, and Chakra UI to build a blog application with CRUD functions without setting up any server. Feel free to comment on what you learned, what we did not cover, possible improvements, and also any questions you might have. I will be glad to take your feedback and answer your questions.
Resources
Here are some resources that might be helpful:
-
Appwrite
Appwrite - The Open Source Firebase alternative introduces iOS support . Appwrite is an open source backend server that helps you build native iOS applications much faster with realtime APIs for authentication, databases, files storage, cloud functions and much more!
-
For our user interface, we will be using Next.js and Chakra UI, our APIs will be stored in the Next.js api directory, and our dynamic pages in the pages directory.